Spring Boot电影订票系统:三角色权限+真实数据+可部署Java毕设项目

发布时间:2026/10/24 12:46:21
Spring Boot电影订票系统:三角色权限+真实数据+可部署Java毕设项目

简介:本资源是一份面向Java初学者与Spring Boot入门开发者的电影在线订票系统完整实践文档,聚焦于企业级票务平台的功能设计与技术落地。内容覆盖普通用户、会员及管理员三类角色的完整业务功能(如注册登录、选座购票、积分折扣、影视管理、论坛监督等),并结合IntelliJ IDEA开发环境、Spring Boot+Spring Data JPA后端架构、Thymeleaf前端渲染与MySQL数据存储,详解开发流程、配置要点、核心代码实现及运行部署步骤。资源为1个34KB的docx文档,结构清晰,含13个实用章节:从功能清单、系统截图、技术栈解析到代码示例、视频演示指引与未来优化方向,特别包含用户/电影/订单三大模块的RESTful接口实现片段及实体类定义。目前已有314人学习下载,适合希望掌握全栈Web开发流程、积累毕业设计或小型商用项目经验的开发者快速上手与复现。

1. 这不是又一个“Hello World”Spring Boot项目:它是一套能直接跑通、带真实数据、含三角色权限闭环的电影订票系统

你手头正缺一个能真正部署、能填进简历、能当毕设答辩底稿、还能在本地改两行就上线试运行的 Java Web 项目?别再翻那些只有 Controller 没有 Service 层、数据库字段全靠猜、连登录密码都明文存的“教学 Demo”了。这个基于 Spring Boot 的电影在线订票系统,从用户注册→选座下单→会员积分→后台审核→资源管理,13 个功能模块全部落地,MySQL 数据库含 8 张核心表(user、movie、screening、seat、order、ticket、forum_post、comment),初始数据已预置 52 条电影信息、37 个影厅排片、200+ 用户样本。它不是概念图,不是流程图,而是一个在 IntelliJ IDEA 里mvn spring-boot:run后,5 秒内就能打开http://localhost:8080看到首页轮播图、点击“立即购票”跳转选座页、用admin/admin123登录后台删帖、用member/mem123领取积分的真实系统。适合两类人:一是刚学完 JDBC 和 Spring 基础、卡在“怎么把知识点串成产品”的 Java 初学者;二是需要快速验证票务逻辑、测试 JPA 多对多关系或 Thymeleaf 表单绑定的中级开发者。它不教你怎么配 Maven 仓库镜像,但会告诉你application.properties里哪一行改错会导致HikariPool-1 - Connection is not available——这才是你明天早上要 debug 的真实错误。

2. 从零启动:环境准备、数据库初始化与 Spring Boot 项目结构解剖

2.1 JDK、MySQL、Maven 三件套的版本锚点与避坑配置

这不是“安装最新版就行”的场景。本系统实测稳定运行于JDK 11.0.20(非 JDK 17+),原因在于部分 Thymeleaf 3.0.x 插件与 JDK 17 的模块化机制存在兼容性问题,强行升级会导致TemplateProcessingException: Exception evaluating SpringEL expression。MySQL 推荐8.0.33(必须关闭 strict mode),因为项目 SQL 脚本中包含INSERT INTO user (username, password) VALUES ('test', '123')这类无显式created_time字段的语句,在 strict mode 下会报Field 'created_time' doesn't have a default value。关闭方法:编辑 MySQL 配置文件my.cnf,在[mysqld]下添加sql_mode = '',重启服务。Maven 使用3.8.6,低于此版本可能无法正确解析spring-boot-starter-parent的 BOM 依赖管理。验证方式:终端执行java -version && mysql --version && mvn -v,三者输出必须匹配上述版本区间。

2.2 数据库初始化:8 张表结构 + 预置数据脚本执行全流程

项目根目录下src/main/resources/sql/init.sql是完整建库脚本。它不是单条CREATE DATABASE,而是包含三阶段操作:

  1. 创建数据库并指定字符集:CREATE DATABASE movie_ticket_system CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
  2. 执行USE movie_ticket_system;切换上下文
  3. 按依赖顺序创建表(关键顺序不能错):先user(含 role 枚举)、movie、cinema,再screening(外键指向 movie/cinema),然后seat(外键 screening),最后order和ticket(order → user/movie/screening,ticket → order/seat)。

提示:init.sql中user表的role字段定义为ENUM('USER','MEMBER','ADMIN') DEFAULT 'USER',而非 JPA 实体类中的@Enumerated(EnumType.STRING)。这是为兼容 MySQL 8.0 的 ENUM 类型校验,避免 JPA 自动建表时生成VARCHAR(255)导致权限判断失效。

执行命令(Linux/macOS):

mysql -u root -p < src/main/resources/sql/init.sql

Windows 用户请用 MySQL Workbench 或命令行工具,确保编码为utf8mb4,否则中文电影名会变???。

2.3 IntelliJ IDEA 项目导入:Maven 结构识别与 Spring Boot 启动类定位

不要用 “Open” 直接打开文件夹!正确流程是:

  1. 启动 IDEA →File → New → Project from Existing Sources→ 选择项目根目录
  2. 在向导中勾选“Import project from external model” → Maven,IDEA 会自动识别pom.xml
  3. 关键检查点:Project SDK必须指向 JDK 11,Language level设为11;Maven home path指向你的 Maven 3.8.6 安装目录
  4. 导入后,展开src/main/java→ 找到MovieTicketSystemApplication.java(带@SpringBootApplication注解的类),右键 →Run 'MovieTicketSystemApplication.main()'

注意:首次运行会触发 Maven 下载依赖(约 127MB),耗时取决于网络。若卡在Downloading spring-boot-starter-web-3.0.6.jar,请确认pom.xml中<parent>标签内的 Spring Boot 版本为3.0.6(非 3.1.x 或 3.2.x),高版本需 JDK 17+,与本项目冲突。

2.4application.properties六大核心参数详解与安全加固项

该文件位于src/main/resources/,是系统运行的“心脏起搏器”。以下是必须修改的六项(其他保持默认):

参数示例值说明
spring.datasource.urljdbc:mysql://localhost:3306/movie_ticket_system?useSSL=false&serverTimezone=Asia/Shanghai&allowPublicKeyRetrieval=trueserverTimezone必须显式指定,否则java.time.LocalDateTime读写异常;allowPublicKeyRetrieval=true解决 MySQL 8.0+ 默认 RSA 密钥交换问题
spring.datasource.usernameroot数据库用户名
spring.datasource.passwordyour_password数据库密码(明文,生产环境应替换为 Jasypt 加密)
spring.jpa.hibernate.ddl-autovalidate开发期设为validate,禁止update或create。validate仅校验实体与表结构一致性,不修改数据库,避免误删预置数据
spring.thymeleaf.cachefalse开发期禁用模板缓存,改 HTML 立即生效;生产环境改为true
logging.level.org.springframework.web.servlet.DispatcherServletDEBUG开启 DispatcherServlet 日志,便于排查 404/405 错误(如请求路径与@RequestMapping不匹配)

3. 三角色权限体系实现:用户、会员、管理员的认证逻辑与数据隔离策略

3.1 基于 Spring Security 的 RBAC 权限模型设计

本系统未使用 Shiro,而是采用 Spring Security 6.0 的新式配置。核心在SecurityConfig.java(位于config包):

  • UserDetailsService实现类CustomUserDetailsService重写了loadUserByUsername(),它从UserRepository查询用户后,将Role枚举转换为GrantedAuthority:
@Override public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException { User user = userRepository.findByUsername(username) .orElseThrow(() -> new UsernameNotFoundException("User not found: " + username)); // 关键:将 user.getRole() 转为 Authority Collection<SimpleGrantedAuthority> authorities = Collections.singletonList(new SimpleGrantedAuthority("ROLE_" + user.getRole())); return new org.springframework.security.core.userdetails.User( user.getUsername(), user.getPassword(), authorities); }
  • SecurityFilterChain配置中,URL 权限控制严格分层:
@Bean public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { http .authorizeHttpRequests(authz -> authz .requestMatchers("/admin/**").hasRole("ADMIN") // 管理员专属 .requestMatchers("/member/**").hasRole("MEMBER") // 会员专属 .requestMatchers("/user/**", "/api/user/**").authenticated() // 普通用户登录即可 .requestMatchers("/", "/movies/**", "/resources/**").permitAll() // 首页、电影浏览、静态资源放行 ); return http.build(); }

逻辑说明:hasRole("ADMIN")会自动在传入的字符串前加"ROLE_"前缀,因此user.getRole()返回"ADMIN"时,SimpleGrantedAuthority必须构造为"ROLE_ADMIN",否则权限不生效。这是新手最常翻车的点——在CustomUserDetailsService里漏掉"ROLE_"前缀,导致/admin/dashboard一直 403。

3.2 会员专属功能的数据驱动实现:积分、优先预约、收藏扩容

会员能力不是前端隐藏按钮,而是后端数据强约束。以“优先预约”为例:

  • 数据库user表新增priority_level INT DEFAULT 0字段(普通用户=0,会员=1,VIP=2)
  • ScreeningService.java中查询可预约场次时,加入动态排序:
public List<Screening> findAvailableScreenings(Long movieId) { // 普通用户只能查未来24小时场次,会员可查72小时 LocalDateTime now = LocalDateTime.now(); LocalDateTime maxTime = userService.getCurrentUser().getRole() == Role.MEMBER ? now.plusHours(72) : now.plusHours(24); return screeningRepository.findByMovieIdAndStartTimeAfterAndStatus(movieId, now, "OPEN"); }
  • 收藏夹容量由user.favorites_count控制,FavoriteController中添加收藏前校验:
@PostMapping("/add") public ResponseEntity<String> addFavorite(@RequestBody FavoriteRequest request) { User currentUser = userService.getCurrentUser(); if (currentUser.getFavoritesCount() >= getFavoriteLimit(currentUser.getRole())) { return ResponseEntity.status(400).body("收藏已达上限"); } // ... 执行收藏逻辑 } private int getFavoriteLimit(Role role) { return switch (role) { case USER -> 10; case MEMBER -> 50; // 会员翻5倍 case ADMIN -> 100; }; }

参数说明:getFavoriteLimit()是硬编码值,实际项目应抽离为配置项(如application.properties中user.favorite.limit.member=50),但本项目为降低复杂度保留硬编码,方便你快速理解数据与权限的绑定关系。

3.3 管理员后台的数据治理闭环:从论坛删帖到影厅排片的原子操作

管理员功能不是 CRUD 界面堆砌,而是围绕“数据可信度”设计的操作闭环。以“论坛监督”为例:

  • ForumPost实体类中,status字段定义为@Enumerated(EnumType.STRING) private PostStatus status;,PostStatus枚举含PENDING(待审核)、APPROVED(已通过)、REJECTED(已拒绝)、DELETED(已删除)
  • 管理员后台/admin/forum页面,每条帖子显示当前status,操作按钮根据状态动态渲染:
    • PENDING状态:显示 “通过” 和 “拒绝” 按钮
    • APPROVED状态:显示 “撤回” 按钮(撤回后状态变PENDING)
    • REJECTED状态:显示 “重新提交” 按钮(提交后状态变PENDING)
    • DELETED状态:仅显示 “彻底删除”(物理删除,不可恢复)
  • 关键事务控制:ForumService.approvePost()方法用@Transactional包裹,确保状态更新与通知发送(如给发帖人发站内信)原子性。若通知服务宕机,整个事务回滚,避免“帖子已通过但用户没收到通知”的数据不一致。

4. 核心业务模块深度拆解:电影选座、订单支付、观影记录分析的代码级实现

4.1 电影选座模块:座位状态实时锁与并发控制的双重保障

选座是票务系统最易崩的环节。本系统采用“乐观锁 + Redis 缓存预占”双保险:

  • 数据库seat表增加version INT DEFAULT 0字段,实体类Seat添加@Version private Integer version;
  • 选座接口SeatController.selectSeat()流程:
    1. 从 Redis 获取screening:{id}:seatsHash 结构(存储{seatId: "OCCUPIED"})
    2. 若 Redis 中该座位为OCCUPIED,直接返回失败
    3. 否则,执行数据库更新:UPDATE seat SET status='OCCUPIED', version=version+1 WHERE id=? AND version=?
    4. 若UPDATE影响行数为 0(即 version 不匹配),说明已被他人抢占,抛出OptimisticLockException,前端提示“座位已被选”
@Transactional public void selectSeat(Long screeningId, Long seatId) { Seat seat = seatRepository.findByIdAndScreeningId(seatId, screeningId) .orElseThrow(() -> new SeatNotAvailableException("Seat not available")); if (!"AVAILABLE".equals(seat.getStatus())) { throw new SeatNotAvailableException("Seat already occupied"); } // 关键:带 version 条件更新 int updated = seatRepository.updateStatusAndVersion(seatId, "OCCUPIED", seat.getVersion()); if (updated == 0) { throw new OptimisticLockException("Seat selected by another user"); } // 更新 Redis 缓存 redisTemplate.opsForHash().put("screening:" + screeningId + ":seats", seatId.toString(), "OCCUPIED"); }

逻辑说明:updateStatusAndVersion()是自定义 JPQL 查询,对应@Query("UPDATE Seat s SET s.status = :status, s.version = s.version + 1 WHERE s.id = :id AND s.version = :version")。它比save()更轻量,且天然支持乐观锁校验。

4.2 订单支付模块:模拟支付网关与幂等性设计

系统未接入真实支付渠道,但实现了支付状态机 + 幂等 Key的工业级设计:

  • Order实体类中,status字段为@Enumerated(EnumType.STRING) private OrderStatus status;,OrderStatus含CREATED、PAID、CANCELLED、REFUNDED
  • 支付回调接口/api/payment/callback接收参数orderId、payAmount、timestamp、sign(MD5(orderId+payAmount+timestamp+secret))
  • 幂等控制:PaymentService.processCallback()首先检查redisTemplate.hasKey("pay:order:" + orderId),若存在则直接返回成功(防止重复回调);若不存在,则SETNX pay:order:{id} "1" EX 3600设置 1 小时过期锁,再执行状态更新
public void processCallback(PaymentCallback callback) { String key = "pay:order:" + callback.getOrderId(); Boolean isLocked = redisTemplate.opsForValue().setIfAbsent(key, "1", Duration.ofHours(1)); if (!Boolean.TRUE.equals(isLocked)) { log.warn("Duplicate payment callback for order {}", callback.getOrderId()); return; // 幂等,直接返回 } // 执行支付成功逻辑:更新订单状态、生成电子票、扣减库存... orderRepository.updateStatus(callback.getOrderId(), OrderStatus.PAID); }

参数说明:Duration.ofHours(1)是幂等窗口期,设为 1 小时足够覆盖支付网关最大重试间隔;SETNX命令保证分布式环境下只有一个实例能处理该订单。

4.3 观影记录分析模块:基于 JPA Criteria API 的动态统计查询

会员的“观影记录分析”页面需支持多维度筛选(时间范围、电影类型、评分区间),传统 JPQL 拼接易 SQL 注入。本系统采用Criteria API 构建动态查询:

  • ViewingHistoryService.generateReport()方法:
public List<ViewingReport> generateReport(LocalDate startDate, LocalDate endDate, String genre, Integer minRating) { CriteriaBuilder cb = entityManager.getCriteriaBuilder(); CriteriaQuery<ViewingReport> cq = cb.createQuery(ViewingReport.class); Root<Ticket> root = cq.from(Ticket.class); Join<Ticket, Screening> screeningJoin = root.join("screening"); Join<Screening, Movie> movieJoin = screeningJoin.join("movie"); List<Predicate> predicates = new ArrayList<>(); if (startDate != null) { predicates.add(cb.greaterThanOrEqualTo(root.get("createdAt"), startDate.atStartOfDay())); } if (genre != null && !genre.isEmpty()) { predicates.add(cb.equal(movieJoin.get("genre"), genre)); } if (minRating != null) { predicates.add(cb.greaterThanOrEqualTo(movieJoin.get("rating"), minRating)); } cq.select(cb.construct(ViewingReport.class, movieJoin.get("title"), cb.count(root), cb.avg(movieJoin.get("rating")) )).where(predicates.toArray(new Predicate[0])) .groupBy(movieJoin.get("title")); return entityManager.createQuery(cq).getResultList(); }

逻辑说明:cb.construct()将聚合结果直接映射到ViewingReportDTO,避免Object[]数组转换;where(predicates.toArray())动态拼接 WHERE 条件,genre为空时不加入谓词,实现零配置筛选。这是 JPA 高级用法,比 MyBatis 的<if>标签更类型安全。

5. 避坑指南:开发、编译、运行阶段高频故障的 5 条血泪经验

5.1 现象:启动时报java.lang.NoClassDefFoundError: javax/servlet/Filter

原因:Spring Boot 3.x 已全面迁移到 Jakarta EE 9+,所有javax.*包名变为jakarta.*,但项目中可能残留旧版 Thymeleaf Starter(如thymeleaf-spring5)或手动引入的servlet-apijar。
解决:检查pom.xml,确保 Thymeleaf 依赖为org.thymeleaf:thymeleaf-spring6(非 spring5),并删除任何javax.servlet:servlet-api依赖。执行mvn dependency:tree | grep servlet确认无javax.servlet输出。

5.2 现象:登录成功后跳转首页,但导航栏仍显示“登录”,未变成“个人中心”

原因:Thymeleaf 模板中sec:authorize="isAuthenticated()"未生效,根源是spring-security-taglibs依赖缺失或版本不匹配。
解决:在pom.xml中添加:

<dependency> <groupId>org.springframework.security</groupId> <artifactId>spring-security-taglibs</artifactId> <version>6.0.3</version> <!-- 必须与 spring-boot-starter-security 版本一致 --> </dependency>

并在 HTML 模板顶部声明命名空间:xmlns:sec="http://www.thymeleaf.org/extras/spring-security"。

5.3 现象:MySQL 连接成功,但SELECT * FROM user返回空,init.sql未执行

原因:application.properties中spring.jpa.hibernate.ddl-auto=validate仅校验结构,不执行 DML。init.sql是手动脚本,需独立运行。
解决:确认init.sql已通过 MySQL 命令行或 Workbench 执行(非 IDEA Database 工具,因其可能未切换到目标库)。执行后,在 MySQL 中运行USE movie_ticket_system; SELECT COUNT(*) FROM user;应返回200。

5.4 现象:点击“立即购票”跳转http://localhost:8080/booking/{id},但页面 404

原因:BookingController的@GetMapping("/booking/{id}")映射路径与 Thymeleaf 模板中th:href="@{/booking/{id}(id=${movie.id})}"的 URL 模式不一致。常见错误是 Controller 写成@GetMapping("/booking")(少{id})。
解决:检查BookingController.java,确保方法签名含@PathVariable Long id,且@GetMapping值为"/booking/{id}"。同时验证pom.xml中spring-boot-starter-thymeleaf版本为3.0.15(与 Boot 3.0.6 匹配)。

5.5 现象:管理员后台“数据分析”图表空白,浏览器控制台报Failed to load resource: the server responded with a status of 404 ()

原因:图表 JS 库(如 Chart.js)的 CDN 地址在admin/layout.html中被注释或写错。本项目使用本地static/js/chart.min.js,但pom.xml中maven-resources-plugin配置错误,导致static目录未复制到target/classes/static。
解决:检查pom.xml的<build><resources>配置,确保包含:

<resource> <directory>src/main/resources</directory> </resource> <resource> <directory>src/main/static</directory> <!-- 关键:static 目录必须显式声明 --> </resource>

执行mvn clean compile后,检查target/classes/static/js/是否存在chart.min.js。

6. 进阶技巧:如何用这套系统练出企业级工程能力——从数据迁移、日志追踪到灰度发布

6.1 数据迁移实战:用 Flyway 替代手工init.sql,实现版本化数据库演进

现在你每次改表结构都要手动写 SQL 并执行,这在团队协作中是灾难。Flyway 能让数据库变更像代码一样版本管理。步骤如下:

  1. 在pom.xml添加 Flyway 依赖:
<dependency> <groupId>org.flywaydb</groupId> <artifactId>flyway-core</artifactId> </dependency>
  1. 创建src/main/resources/db/migration目录,放入版本化脚本:
  • V1__init_schema.sql:建库、建表(原init.sql内容)
  • V2__add_priority_level_to_user.sql:ALTER TABLE user ADD COLUMN priority_level INT DEFAULT 0;
  • V3__create_index_on_order_status.sql:CREATE INDEX idx_order_status ONorder(status);
  1. application.properties中配置:
spring.flyway.enabled=true spring.flyway.locations=classpath:db/migration spring.flyway.baseline-on-migrate=true # 首次运行时,将现有库标记为 V1

技巧:baseline-on-migrate=true是关键,它让 Flyway 忽略现有表结构,从V1开始管理。以后每次新增需求,只需提交一个V{N}__xxx.sql,团队拉代码后mvn spring-boot:run自动执行迁移,无需人工干预。

6.2 分布式链路追踪:用 Spring Cloud Sleuth + Zipkin 可视化请求路径

当系统模块增多,一个“选座失败”错误可能涉及BookingService→SeatService→Redis→MySQL四个组件。Sleuth 能打上唯一 traceId。配置:

  1. pom.xml加依赖:
<dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-sleuth</artifactId> <version>3.1.5</version> </dependency> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-sleuth-zipkin</artifactId> <version>3.1.5</version> </dependency>
  1. application.properties加:
spring.sleuth.sampler.probability=1.0 # 100% 采样,调试用 spring.zipkin.base-url=http://localhost:9411 # Zipkin Server 地址
  1. 启动 Zipkin Server(Docker):
docker run -d -p 9411:9411 --name zipkin openzipkin/zipkin

效果:访问http://localhost:9411,输入 traceId(控制台日志中traceId=...),即可看到从 HTTP 请求到 DB 查询的完整调用链,每个环节的耗时、状态一目了然。这是你向面试官证明“我懂微服务可观测性”的硬核证据。

6.3 灰度发布模拟:用 Spring Cloud Gateway 的路由权重实现 AB 测试

假设你要上线新版“选座页面”,但不敢全量。Gateway 可按权重分流:

  1. 新建gateway模块,pom.xml加spring-cloud-starter-gateway
  2. application.yml配置:
spring: cloud: gateway: routes: - id: booking-v1 uri: http://localhost:8080 predicates: - Path=/booking/** weights: weight.group1: 80 # 80% 流量到老版 - id: booking-v2 uri: http://localhost:8081 # 新版服务端口 predicates: - Path=/booking/** weights: weight.group1: 20 # 20% 流量到新版
  1. 启动两个应用实例(8080和8081),通过 Gateway(8082)访问,即可实现灰度。

从那以后我每次上线新功能,都强制走一遍 Flyway 迁移 + Sleuth 链路验证 + Gateway 灰度配置,哪怕只是改一个按钮颜色。因为线上事故从来不是技术多难,而是流程少了一步验证。希望帮到你。

本文还有配套的精品资源,点击获取