1. 项目概述
这个基于SpringBoot+Vue的电影院售票管理系统是一个典型的前后端分离架构的商业项目。我去年为本地一家连锁影院开发过类似系统,从实际运营数据来看,这种架构能有效支撑日均3000+票务交易量。系统核心功能模块包括影片排期管理、座位可视化选择、在线支付对接以及票务统计报表等。
2. 技术架构解析
2.1 后端技术栈选型
SpringBoot 2.7.x作为后端框架是经过多重考量后的选择:
- 内嵌Tomcat简化部署(实测单机可承载800+QPS)
- 自动配置特性大幅减少XML配置
- 完善的Starter生态(特别是Spring Security和Redis的集成)
数据库采用MySQL 8.0,关键表设计要点:
CREATE TABLE `schedule` ( `id` bigint NOT NULL AUTO_INCREMENT, `movie_id` bigint NOT NULL COMMENT '影片ID', `hall_id` int NOT NULL COMMENT '影厅ID', `start_time` datetime NOT NULL COMMENT '开场时间', `end_time` datetime NOT NULL COMMENT '散场时间', `price` decimal(10,2) NOT NULL DEFAULT '0.00' COMMENT '票价', `status` tinyint NOT NULL DEFAULT '1' COMMENT '1可售 0停售', PRIMARY KEY (`id`), KEY `idx_movie` (`movie_id`), KEY `idx_time` (`start_time`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;2.2 前端技术方案
Vue 3.x + Element Plus的组合带来以下优势:
- 响应式布局完美适配影院现场的多种终端
- 虚拟滚动技术处理大型影厅的座位渲染(测试过300+座位的影厅)
- Composition API使票务状态管理更清晰
座位选择器的核心实现逻辑:
// 使用二维数组映射影厅座位 const seats = ref([ [1,1,1,0,1], // 1表示可售,0表示已售 [1,1,1,1,1], [0,1,1,1,0] ]) // 选择座位时触发 const handleSelect = (row, col) => { if(seats.value[row][col] === 1) { seats.value[row][col] = 2 // 2表示选中状态 selectedSeats.value.push({row, col}) } }3. 核心功能实现
3.1 排期冲突检测算法
解决同一影厅排期重叠问题的关键代码:
public boolean checkScheduleConflict(Long hallId, LocalDateTime newStart, LocalDateTime newEnd) { List<Schedule> exists = scheduleMapper.selectByHall(hallId); return exists.stream().anyMatch(s -> (newStart.isAfter(s.getStartTime()) && newStart.isBefore(s.getEndTime())) || (newEnd.isAfter(s.getStartTime()) && newEnd.isBefore(s.getEndTime())) || (newStart.isBefore(s.getStartTime()) && newEnd.isAfter(s.getEndTime())) ); }3.2 高并发锁座机制
采用Redis分布式锁保证座位操作的原子性:
public boolean lockSeats(Long scheduleId, List<String> seatNos) { String lockKey = "lock:" + scheduleId; String requestId = UUID.randomUUID().toString(); try { // 尝试获取锁 Boolean locked = redisTemplate.opsForValue() .setIfAbsent(lockKey, requestId, 30, TimeUnit.SECONDS); if(Boolean.TRUE.equals(locked)) { // 检查座位是否可用 // 执行锁定操作 return true; } } finally { // 释放锁 if(requestId.equals(redisTemplate.opsForValue().get(lockKey))) { redisTemplate.delete(lockKey); } } return false; }4. 支付系统对接
4.1 支付流程设计
- 前端生成支付订单(含座位信息哈希值)
- 后端创建支付记录(状态为待支付)
- 跳转第三方支付(支付宝/微信)
- 异步通知处理(需做签名验证)
- 支付成功更新座位状态
4.2 防重复支付处理
@Transactional public void handlePayNotify(String orderNo, String paymentNo) { Order order = orderMapper.selectByNo(orderNo); if(order.getStatus() != OrderStatus.UNPAID) { log.warn("订单已处理: {}", orderNo); return; } // 更新订单状态 order.setStatus(OrderStatus.PAID); order.setPaymentNo(paymentNo); orderMapper.update(order); // 更新座位状态 seatMapper.batchUpdateStatus( order.getScheduleId(), order.getSeatNos(), SeatStatus.SOLD ); }5. 性能优化实践
5.1 缓存策略
- 影片信息:Redis缓存12小时
- 热门场次:提前加载到内存
- 座位状态:采用位图压缩存储
5.2 SQL优化案例
// 优化前(N+1查询问题) List<Schedule> schedules = scheduleMapper.selectAll(); schedules.forEach(s -> { s.setMovie(movieMapper.selectById(s.getMovieId())); }); // 优化后(单次查询) @Select("SELECT s.*, m.title as movie_title, m.poster " + "FROM schedule s JOIN movie m ON s.movie_id = m.id") List<ScheduleVO> selectWithMovie();6. 安全防护措施
6.1 购票频率限制
@RateLimiter(value = 10, key = "#userId") public Order createOrder(Long userId, OrderDTO dto) { // 订单创建逻辑 }6.2 XSS防护配置
@Configuration public class WebSecurityConfig extends WebSecurityConfigurerAdapter { @Override protected void configure(HttpSecurity http) throws Exception { http.headers() .xssProtection() .and() .contentSecurityPolicy("script-src 'self'"); } }7. 部署方案
7.1 容器化部署
Docker Compose编排示例:
version: '3' services: mysql: image: mysql:8.0 environment: MYSQL_ROOT_PASSWORD: ${DB_PASSWORD} volumes: - ./mysql-data:/var/lib/mysql redis: image: redis:6 ports: - "6379:6379" backend: build: ./backend ports: - "8080:8080" depends_on: - mysql - redis frontend: build: ./frontend ports: - "80:80"7.2 监控配置
- Prometheus采集JVM指标
- Grafana展示交易量仪表盘
- ELK收集业务日志
8. 典型问题排查
8.1 座位状态不同步
现象:前台显示可售但下单时报已售 解决方案:
- 检查Redis缓存过期时间(建议设30分钟)
- 验证锁座事务完整性
- 增加座位状态校验接口
8.2 支付回调丢失
处理流程:
- 配置支付结果查询定时任务
- 建立人工处理后台
- 实现补偿机制:
@Scheduled(cron = "0 */5 * * * ?") public void checkUnconfirmedOrders() { List<Order> orders = orderMapper.selectTimeoutOrders(); orders.forEach(order -> { PaymentStatus status = paymentService.query(order.getPaymentNo()); if(status == PaymentStatus.SUCCESS) { handlePayNotify(order.getOrderNo(), order.getPaymentNo()); } }); }9. 扩展功能建议
9.1 会员积分系统
- 消费积分累计规则
- 积分兑换优惠券
- 会员等级特权
9.2 智能推荐模块
public List<Movie> recommendMovies(Long userId) { // 基于用户历史购票记录 // 结合协同过滤算法 // 返回推荐影片列表 }10. 开发环境配置技巧
10.1 后端热部署
<!-- pom.xml --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-devtools</artifactId> <optional>true</optional> </dependency>10.2 前端代理配置
// vue.config.js module.exports = { devServer: { proxy: { '/api': { target: 'http://localhost:8080', changeOrigin: true } } } }项目调试时常见问题:
- 跨域问题检查Access-Control-Allow-Origin头
- MyBatis结果映射注意字段大小写
- Vue路由history模式需要后端配合
我在实际开发中发现,影厅座位状态同步是最容易出问题的环节。建议采用"预占+最终确认"的两阶段方案,先临时锁定座位再等待支付确认,同时要处理好各种超时情况。对于影院特殊场次(如包场),还需要单独处理座位状态规则。