1. 项目概述影院订票系统的全栈实现去年接手本地一家小型影院的数字化改造需求时我选择了SpringBootVue的全栈方案。这个看似常见的组合在实际落地时会遇到票务库存的实时性、座位图的动态渲染、支付接口的稳定性等具体挑战。本文将分享从技术选型到部署上线的完整过程特别适合需要毕业设计实战案例的计算机专业学生以及准备转型全栈开发的Java工程师。系统采用经典的三层架构Vue3构建的前端实现响应式座位选择界面SpringBoot处理高并发的订票请求MySQL通过事务和锁机制保证数据一致性。在3个月的开发周期里我们实现了包括排期管理、在线选座、支付对接、票务核销等核心功能峰值时能支撑每秒200的订票请求。2. 技术架构解析2.1 前端技术栈选型选用Vue3Element Plus的组合主要基于三点考虑座位图需要频繁的DOM操作Vue的响应式机制比直接操作DOM更高效Element Plus的表格和表单组件能快速搭建后台管理系统组合式API更适合处理复杂的座位状态逻辑关键代码示例座位组件template div classseat :class{occupied: isOccupied, selected: isSelected} clicktoggleSelect {{ seatNumber }} /div /template script setup const props defineProps({ seatNumber: String, isOccupied: Boolean }) const isSelected ref(false) const toggleSelect () { if(!props.isOccupied) { isSelected.value !isSelected.value emit(selection-change, props.seatNumber, isSelected.value) } } /script2.2 后端核心设计SpringBoot的配置要点# application.yml关键配置 spring: datasource: url: jdbc:mysql://localhost:3306/cinema?useSSLfalseserverTimezoneAsia/Shanghai username: root password: 加密密码 redis: host: 127.0.0.1 port: 6379 password: database: 1 jackson: date-format: yyyy-MM-dd HH:mm:ss time-zone: GMT8订票服务的分布式锁实现public boolean lockSeats(ListInteger seatIds, String sessionId) { String lockKey lock:session: sessionId; // 使用Redis的SETNX实现分布式锁 Boolean locked redisTemplate.opsForValue().setIfAbsent(lockKey, locked, 30, TimeUnit.SECONDS); if (Boolean.TRUE.equals(locked)) { try { // 检查座位是否可用 ListSeat seats seatMapper.selectBatchIds(seatIds); if (seats.stream().anyMatch(s - s.getStatus() ! 0)) { return false; } // 更新座位状态 seatMapper.updateStatusBatch(seatIds, 1); // 1表示锁定状态 return true; } finally { redisTemplate.delete(lockKey); } } return false; }3. 数据库设计要点3.1 核心表结构CREATE TABLE film ( id int NOT NULL AUTO_INCREMENT, name varchar(100) NOT NULL COMMENT 电影名称, duration int NOT NULL COMMENT 时长(分钟), poster_url varchar(255) DEFAULT NULL, description text, PRIMARY KEY (id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; CREATE TABLE schedule ( id int NOT NULL AUTO_INCREMENT, film_id int NOT NULL, hall_id int NOT NULL, start_time datetime NOT NULL, price decimal(10,2) NOT NULL, remaining_seats int NOT NULL DEFAULT 0, PRIMARY KEY (id), KEY idx_film (film_id), KEY idx_time (start_time) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; CREATE TABLE seat ( id int NOT NULL AUTO_INCREMENT, hall_id int NOT NULL, row_num varchar(10) NOT NULL, col_num int NOT NULL, status tinyint NOT NULL DEFAULT 0 COMMENT 0-可用 1-锁定 2-已售, schedule_id int DEFAULT NULL, PRIMARY KEY (id), UNIQUE KEY uk_hall_seat (hall_id,row_num,col_num,schedule_id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;3.2 事务处理示例购票事务的Java实现Transactional public Order createOrder(OrderDTO orderDTO) { // 1. 检查排期是否存在 Schedule schedule scheduleMapper.selectById(orderDTO.getScheduleId()); if (schedule null) { throw new BusinessException(排期不存在); } // 2. 锁定座位已在前端展示时过滤此处二次验证 ListSeat seats seatMapper.selectBatchIds(orderDTO.getSeatIds()); if (seats.size() ! orderDTO.getSeatIds().size() || seats.stream().anyMatch(s - s.getStatus() ! 0)) { throw new BusinessException(座位已被占用); } // 3. 创建订单 Order order new Order(); order.setUserId(orderDTO.getUserId()); order.setScheduleId(orderDTO.getScheduleId()); order.setTotalPrice(seats.size() * schedule.getPrice()); order.setStatus(0); // 0-待支付 orderMapper.insert(order); // 4. 更新座位状态 seatMapper.updateStatusBatch(orderDTO.getSeatIds(), 2); // 2-已售 scheduleMapper.decrementRemainingSeats(orderDTO.getScheduleId(), seats.size()); // 5. 生成订单明细 ListOrderItem items seats.stream() .map(seat - new OrderItem(order.getId(), seat.getId())) .collect(Collectors.toList()); orderItemMapper.insertBatch(items); return order; }4. 典型问题解决方案4.1 座位并发冲突处理我们采用三级防护策略前端通过WebSocket实时接收座位状态变更服务端Redis分布式锁数据库乐观锁数据库唯一索引防止重复插入WebSocket配置示例Configuration EnableWebSocketMessageBroker public class WebSocketConfig implements WebSocketMessageBrokerConfigurer { Override public void configureMessageBroker(MessageBrokerRegistry config) { config.enableSimpleBroker(/topic); config.setApplicationDestinationPrefixes(/app); } Override public void registerStompEndpoints(StompEndpointRegistry registry) { registry.addEndpoint(/ws) .setAllowedOrigins(*) .withSockJS(); } }4.2 支付超时处理支付状态检查的定时任务Scheduled(fixedRate 60000) // 每分钟检查一次 public void checkPaymentTimeout() { LocalDateTime timeoutPoint LocalDateTime.now().minusMinutes(15); ListOrder unpaidOrders orderMapper.selectTimeoutOrders( timeoutPoint.format(DateTimeFormatter.ofPattern(yyyy-MM-dd HH:mm:ss))); unpaidOrders.forEach(order - { // 释放座位 seatMapper.releaseSeatsByOrder(order.getId()); // 更新订单状态 order.setStatus(3); // 3-已取消 orderMapper.updateById(order); }); }5. 部署实战指南5.1 后端部署关键步骤打包SpringBoot应用mvn clean package -DskipTestsDockerfile示例FROM openjdk:11-jre WORKDIR /app COPY target/cinema-backend-0.0.1-SNAPSHOT.jar app.jar EXPOSE 8080 ENTRYPOINT [java,-jar,app.jar]Nginx配置片段server { listen 80; server_name api.cinema.example.com; location / { proxy_pass http://127.0.0.1:8080; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; } }5.2 前端部署要点Vue项目的环境配置// .env.production VUE_APP_API_BASE_URLhttps://api.cinema.example.com VUE_APP_WS_URLwss://api.cinema.example.com/ws构建命令npm run build部署到Nginx的配置server { listen 80; server_name cinema.example.com; root /var/www/cinema/dist; index index.html; location / { try_files $uri $uri/ /index.html; } }6. 答辩常见问题准备6.1 技术类问题Q如何保证座位选择的实时性 A我们采用WebSocketRedis Pub/Sub的方案。当某个座位状态变化时后端会通过Redis发布消息所有订阅了该影厅频道的客户端都会实时收到更新。6.2 业务类问题Q遇到支付成功但座位未预留的情况怎么处理 A我们有对账机制1) 支付回调时检查订单状态 2) 每日定时任务核对支付记录与座位状态 3) 提供人工修正接口。出现异常时会自动发送告警邮件。6.3 扩展性问题Q系统如何支持节假日的高并发 A可以从三方面优化1) 增加Redis集群节点 2) 对座位数据进行分片存储 3) 前端加入排队机制。我们的压测数据显示当前架构在8核16G服务器上可支撑每秒500的并发请求。