Spring Boot电影订票系统:三角色权限+真实数据+可部署Java毕设项目
简介本资源是一份面向Java初学者与Spring Boot入门开发者的电影在线订票系统完整实践文档聚焦于企业级票务平台的功能设计与技术落地。内容覆盖普通用户、会员及管理员三类角色的完整业务功能如注册登录、选座购票、积分折扣、影视管理、论坛监督等并结合IntelliJ IDEA开发环境、Spring BootSpring 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 doesnt 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而是包含三阶段操作创建数据库并指定字符集CREATE DATABASE movie_ticket_system CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;执行USE movie_ticket_system;切换上下文按依赖顺序创建表关键顺序不能错先user含 role 枚举、movie、cinema再screening外键指向 movie/cinema然后seat外键 screening最后order和ticketorder → user/movie/screeningticket → order/seat。提示init.sql中user表的role字段定义为ENUM(USER,MEMBER,ADMIN) DEFAULT USER而非 JPA 实体类中的Enumerated(EnumType.STRING)。这是为兼容 MySQL 8.0 的 ENUM 类型校验避免 JPA 自动建表时生成VARCHAR(255)导致权限判断失效。执行命令Linux/macOSmysql -u root -p src/main/resources/sql/init.sqlWindows 用户请用 MySQL Workbench 或命令行工具确保编码为utf8mb4否则中文电影名会变???。2.3 IntelliJ IDEA 项目导入Maven 结构识别与 Spring Boot 启动类定位不要用 “Open” 直接打开文件夹正确流程是启动 IDEA →File → New → Project from Existing Sources→ 选择项目根目录在向导中勾选“Import project from external model” → MavenIDEA 会自动识别pom.xml关键检查点Project SDK必须指向 JDK 11Language level设为11Maven home path指向你的 Maven 3.8.6 安装目录导入后展开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?useSSLfalseserverTimezoneAsia/ShanghaiallowPublicKeyRetrievaltrueserverTimezone必须显式指定否则java.time.LocalDateTime读写异常allowPublicKeyRetrievaltrue解决 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 立即生效生产环境改为truelogging.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枚举转换为GrantedAuthorityOverride public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException { User user userRepository.findByUsername(username) .orElseThrow(() - new UsernameNotFoundException(User not found: username)); // 关键将 user.getRole() 转为 Authority CollectionSimpleGrantedAuthority 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会员1VIP2ScreeningService.java中查询可预约场次时加入动态排序public ListScreening 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 ResponseEntityString 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.member50但本项目为降低复杂度保留硬编码方便你快速理解数据与权限的绑定关系。3.3 管理员后台的数据治理闭环从论坛删帖到影厅排片的原子操作管理员功能不是 CRUD 界面堆砌而是围绕“数据可信度”设计的操作闭环。以“论坛监督”为例ForumPost实体类中status字段定义为Enumerated(EnumType.STRING) private PostStatus status;PostStatus枚举含PENDING待审核、APPROVED已通过、REJECTED已拒绝、DELETED已删除管理员后台/admin/forum页面每条帖子显示当前status操作按钮根据状态动态渲染PENDING状态显示 “通过” 和 “拒绝” 按钮APPROVED状态显示 “撤回” 按钮撤回后状态变PENDINGREJECTED状态显示 “重新提交” 按钮提交后状态变PENDINGDELETED状态仅显示 “彻底删除”物理删除不可恢复关键事务控制ForumService.approvePost()方法用Transactional包裹确保状态更新与通知发送如给发帖人发站内信原子性。若通知服务宕机整个事务回滚避免“帖子已通过但用户没收到通知”的数据不一致。4. 核心业务模块深度拆解电影选座、订单支付、观影记录分析的代码级实现4.1 电影选座模块座位状态实时锁与并发控制的双重保障选座是票务系统最易崩的环节。本系统采用“乐观锁 Redis 缓存预占”双保险数据库seat表增加version INT DEFAULT 0字段实体类Seat添加Version private Integer version;选座接口SeatController.selectSeat()流程从 Redis 获取screening:{id}:seatsHash 结构存储{seatId: OCCUPIED}若 Redis 中该座位为OCCUPIED直接返回失败否则执行数据库更新UPDATE seat SET statusOCCUPIED, versionversion1 WHERE id? AND version?若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、signMD5(orderIdpayAmounttimestampsecret)幂等控制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 ListViewingReport generateReport(LocalDate startDate, LocalDate endDate, String genre, Integer minRating) { CriteriaBuilder cb entityManager.getCriteriaBuilder(); CriteriaQueryViewingReport cq cb.createQuery(ViewingReport.class); RootTicket root cq.from(Ticket.class); JoinTicket, Screening screeningJoin root.join(screening); JoinScreening, Movie movieJoin screeningJoin.join(movie); ListPredicate 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:authorizeisAuthenticated()未生效根源是spring-security-taglibs依赖缺失或版本不匹配。解决在pom.xml中添加dependency groupIdorg.springframework.security/groupId artifactIdspring-security-taglibs/artifactId version6.0.3/version !-- 必须与 spring-boot-starter-security 版本一致 -- /dependency并在 HTML 模板顶部声明命名空间xmlns:sechttp://www.thymeleaf.org/extras/spring-security。5.3 现象MySQL 连接成功但SELECT * FROM user返回空init.sql未执行原因application.properties中spring.jpa.hibernate.ddl-autovalidate仅校验结构不执行 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的buildresources配置确保包含resource directorysrc/main/resources/directory /resource resource directorysrc/main/static/directory !-- 关键static 目录必须显式声明 -- /resource执行mvn clean compile后检查target/classes/static/js/是否存在chart.min.js。6. 进阶技巧如何用这套系统练出企业级工程能力——从数据迁移、日志追踪到灰度发布6.1 数据迁移实战用 Flyway 替代手工init.sql实现版本化数据库演进现在你每次改表结构都要手动写 SQL 并执行这在团队协作中是灾难。Flyway 能让数据库变更像代码一样版本管理。步骤如下在pom.xml添加 Flyway 依赖dependency groupIdorg.flywaydb/groupId artifactIdflyway-core/artifactId /dependency创建src/main/resources/db/migration目录放入版本化脚本V1__init_schema.sql建库、建表原init.sql内容V2__add_priority_level_to_user.sqlALTER TABLE user ADD COLUMN priority_level INT DEFAULT 0;V3__create_index_on_order_status.sqlCREATE INDEX idx_order_status ONorder(status);application.properties中配置spring.flyway.enabledtrue spring.flyway.locationsclasspath:db/migration spring.flyway.baseline-on-migratetrue # 首次运行时将现有库标记为 V1技巧baseline-on-migratetrue是关键它让 Flyway 忽略现有表结构从V1开始管理。以后每次新增需求只需提交一个V{N}__xxx.sql团队拉代码后mvn spring-boot:run自动执行迁移无需人工干预。6.2 分布式链路追踪用 Spring Cloud Sleuth Zipkin 可视化请求路径当系统模块增多一个“选座失败”错误可能涉及BookingService→SeatService→Redis→MySQL四个组件。Sleuth 能打上唯一 traceId。配置pom.xml加依赖dependency groupIdorg.springframework.cloud/groupId artifactIdspring-cloud-starter-sleuth/artifactId version3.1.5/version /dependency dependency groupIdorg.springframework.cloud/groupId artifactIdspring-cloud-sleuth-zipkin/artifactId version3.1.5/version /dependencyapplication.properties加spring.sleuth.sampler.probability1.0 # 100% 采样调试用 spring.zipkin.base-urlhttp://localhost:9411 # Zipkin Server 地址启动 Zipkin ServerDockerdocker run -d -p 9411:9411 --name zipkin openzipkin/zipkin效果访问http://localhost:9411输入 traceId控制台日志中traceId...即可看到从 HTTP 请求到 DB 查询的完整调用链每个环节的耗时、状态一目了然。这是你向面试官证明“我懂微服务可观测性”的硬核证据。6.3 灰度发布模拟用 Spring Cloud Gateway 的路由权重实现 AB 测试假设你要上线新版“选座页面”但不敢全量。Gateway 可按权重分流新建gateway模块pom.xml加spring-cloud-starter-gatewayapplication.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% 流量到新版启动两个应用实例8080和8081通过 Gateway8082访问即可实现灰度。从那以后我每次上线新功能都强制走一遍 Flyway 迁移 Sleuth 链路验证 Gateway 灰度配置哪怕只是改一个按钮颜色。因为线上事故从来不是技术多难而是流程少了一步验证。希望帮到你。本文还有配套的精品资源点击获取