1. 项目概述与技术栈选型
这套影院订票系统采用前后端分离架构,后端基于SpringBoot+MyBatis实现业务逻辑,前端使用Vue3构建用户界面,数据库选用MySQL。这种技术组合在当前企业级应用开发中非常典型,下面具体分析各技术选型的考量:
后端技术栈解析:
- SpringBoot 2.7.x:简化了传统Spring应用的初始搭建和开发过程,内嵌Tomcat服务器,通过starter依赖实现自动配置。选择2.7而非3.0版本是考虑到企业环境的稳定性要求。
- MyBatis 3.5.x:相比Hibernate提供了更灵活的SQL控制能力,对于需要复杂查询优化的票务系统尤为重要。通过XML配置实现动态SQL,便于处理影院排期、座位状态等动态条件查询。
前端技术方案:
- Vue3组合式API:相比Options API更利于功能模块的封装和复用,特别适合票务系统中需要频繁交互的选座、支付等组件。
- Pinia状态管理:替代Vuex管理全局状态(如用户登录态、选座信息),其TypeScript支持更好,与Vue3的响应式系统深度集成。
数据库设计要点:
- MySQL 8.0:采用InnoDB引擎保障事务安全,对影院场景下的并发锁座、支付等操作至关重要。配置了读写分离架构,主库处理订单写入,从库支撑查询。
- 关键表设计包含:电影表(film)、影院表(cinema)、放映厅表(hall)、场次表(schedule)、座位表(seat)、订单表(order)等,通过外键建立关联。
提示:实际部署时建议将MySQL的transaction_isolation设置为READ-COMMITTED,平衡并发性能与数据一致性要求。
2. 系统核心功能模块实现
2.1 影院场次管理与座位状态维护
场次管理是系统的核心业务模块,主要涉及以下技术实现:
// ScheduleServiceImpl.java 节选 @Transactional public List<ScheduleVO> getAvailableSchedules(Long cinemaId, LocalDate date) { // 1. 基础查询:获取指定影院日期的场次列表 List<Schedule> schedules = scheduleMapper.selectByCinemaAndDate(cinemaId, date); // 2. 关联查询每个场次的已售座位数 return schedules.stream().map(schedule -> { ScheduleVO vo = new ScheduleVO(); BeanUtils.copyProperties(schedule, vo); vo.setSoldSeats(orderMapper.countSoldSeats(schedule.getId())); return vo; }).collect(Collectors.toList()); }关键技术点:
- 使用MyBatis的
@SelectProvider实现动态SQL,根据查询条件动态生成场次查询语句 - 通过
@Transactional保证座位状态更新的原子性,避免超卖 - 采用乐观锁机制处理并发选座冲突:
<!-- OrderMapper.xml 片段 --> <update id="lockSeats"> UPDATE seat SET status = 'LOCKED', version = version + 1, lock_time = NOW() WHERE id IN <foreach collection="seatIds" item="id" open="(" separator="," close=")"> #{id} </foreach> AND version = #{version} AND status = 'AVAILABLE' </update>2.2 选座与订单支付流程
前端选座界面使用Canvas绘制座位图,实时同步座位状态:
<!-- SeatSelection.vue 核心逻辑 --> <script setup> const seats = ref([]); const selectedSeats = ref(new Set()); // 获取实时座位状态 const fetchSeatsStatus = async (scheduleId) => { const { data } = await axios.get(`/api/seats?scheduleId=${scheduleId}`); seats.value = data; }; // 选座处理 const toggleSelect = (seatId) => { if (seats.value.find(s => s.id === seatId).status !== 'AVAILABLE') return; if (selectedSeats.value.has(seatId)) { selectedSeats.value.delete(seatId); } else { if (selectedSeats.value.size >= 6) { alert('最多选择6个座位'); return; } selectedSeats.value.add(seatId); } }; </script>支付流程关键设计:
- 订单状态机设计:
- PENDING -> PAID(支付成功)
- PENDING -> CANCELLED(用户取消)
- PENDING -> EXPIRED(15分钟未支付)
- 使用Spring Scheduler定时扫描过期订单:
@Scheduled(cron = "0 */5 * * * ?") public void cancelExpiredOrders() { orderMapper.updateExpiredOrders( OrderStatus.PENDING, OrderStatus.EXPIRED, LocalDateTime.now().minusMinutes(15)); }3. 前后端分离架构实践
3.1 接口规范与安全控制
采用RESTful风格设计API,使用JWT进行认证:
// SecurityConfig.java 节选 @Configuration @EnableWebSecurity public class SecurityConfig { @Bean public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { http.csrf().disable() .authorizeRequests() .antMatchers("/api/auth/**").permitAll() .antMatchers("/api/**").authenticated() .and() .addFilter(new JwtAuthenticationFilter(authenticationManager())) .sessionManagement() .sessionCreationPolicy(SessionCreationPolicy.STATELESS); return http.build(); } }接口文档管理:
- 使用Swagger UI生成API文档,通过注解维护接口说明:
@Operation(summary = "获取场次信息") @GetMapping("/schedules") public Result<List<ScheduleVO>> getSchedules( @Parameter(description = "影院ID") @RequestParam Long cinemaId, @Parameter(description = "查询日期") @RequestParam @DateTimeFormat(pattern = "yyyy-MM-dd") LocalDate date) { // ... }3.2 跨域与文件上传处理
Vue3前端配置axios实例:
// http.js const instance = axios.create({ baseURL: import.meta.env.VITE_API_BASE_URL, timeout: 10000, withCredentials: true }); // 请求拦截器 instance.interceptors.request.use(config => { const token = localStorage.getItem('token'); if (token) { config.headers.Authorization = `Bearer ${token}`; } return config; }); // 响应拦截器处理通用错误 instance.interceptors.response.use( response => response.data, error => { if (error.response.status === 401) { router.push('/login'); } return Promise.reject(error); } );文件上传示例(用于电影海报上传):
<template> <input type="file" @change="handleUpload" accept="image/*"> </template> <script setup> const handleUpload = async (e) => { const file = e.target.files[0]; const formData = new FormData(); formData.append('file', file); try { const { url } = await instance.post('/api/upload', formData, { headers: { 'Content-Type': 'multipart/form-data' } }); // 更新电影海报URL } catch (err) { console.error('上传失败:', err); } }; </script>4. 性能优化与安全实践
4.1 数据库查询优化
索引设计:
- 场次表(schedule)建立(cinema_id, show_date)联合索引
- 订单表(order)建立(user_id, status)索引加速用户订单查询
- 使用EXPLAIN分析慢查询,优化SQL执行计划
缓存策略:
// FilmServiceImpl.java @Cacheable(value = "films", key = "#id") public FilmDetailVO getFilmDetail(Long id) { return filmMapper.selectDetailById(id); } // 更新缓存 @CacheEvict(value = "films", key = "#filmId") public void updateFilm(FilmUpdateDTO dto) { filmMapper.updateById(dto); }4.2 安全防护措施
SQL注入防护:
- 严格使用MyBatis参数绑定(#{}),禁用${}拼接
- 使用MyBatis-Plus的SQL注入器进行自动过滤
XSS防护:
- 前端使用vue-dompurify对用户输入进行净化
- 后端对接收的文本内容进行HTML实体编码
CSRF防护:
- 虽然采用JWT无状态认证,仍建议关键操作验证Referer
- 敏感操作(如支付)要求二次验证
性能监控方案:
- 使用Spring Boot Actuator暴露健康检查端点
- 集成Prometheus + Grafana监控系统关键指标
- 日志系统采用ELK栈收集分析异常信息
5. 部署与运维方案
5.1 容器化部署
Docker Compose编排示例:
version: '3' services: mysql: image: mysql:8.0 environment: MYSQL_ROOT_PASSWORD: ${DB_PASSWORD} MYSQL_DATABASE: cinema volumes: - mysql_data:/var/lib/mysql ports: - "3306:3306" backend: build: ./backend environment: SPRING_DATASOURCE_URL: jdbc:mysql://mysql:3306/cinema ports: - "8080:8080" depends_on: - mysql frontend: build: ./frontend ports: - "80:80" volumes: mysql_data:5.2 CI/CD流程
GitLab CI配置示例:
stages: - build - test - deploy backend-build: stage: build script: - cd backend - ./mvnw clean package artifacts: paths: - backend/target/*.jar frontend-build: stage: build script: - cd frontend - npm install - npm run build artifacts: paths: - frontend/dist deploy-prod: stage: deploy script: - scp backend/target/*.jar user@server:/app - ssh user@server "docker-compose up -d --build" only: - main6. 典型问题排查与解决
6.1 座位状态同步异常
现象:用户选座时偶尔出现座位已被占用但仍显示可选的状况。
排查过程:
- 检查前端轮询间隔(设置每15秒获取最新座位状态)
- 验证后端缓存配置(场次座位数据缓存时间不应超过5分钟)
- 分析MySQL锁等待超时日志,调整innodb_lock_wait_timeout参数
- 最终解决方案:在选座操作时增加版本号校验,前端提交选座请求携带座位数据的版本号,后端验证版本一致性。
6.2 支付完成但订单状态未更新
根因分析:支付平台回调时服务端处理时间过长,导致重复回调。
解决方案:
- 实现订单状态变更的幂等处理
- 添加分布式锁防止并发更新
- 日志记录所有回调请求用于对账
@Transactional public void handlePaymentCallback(String orderNo, PaymentStatus status) { // 获取分布式锁 String lockKey = "payment:callback:" + orderNo; try { boolean locked = redisTemplate.opsForValue() .setIfAbsent(lockKey, "1", 5, TimeUnit.MINUTES); if (!locked) { throw new BusinessException("正在处理该订单的支付结果"); } Order order = orderMapper.selectByOrderNo(orderNo); if (order.getStatus() != OrderStatus.PENDING) { return; // 已处理过该订单 } // 更新订单状态 order.setStatus(status == PaymentStatus.SUCCESS ? OrderStatus.PAID : OrderStatus.FAILED); orderMapper.updateById(order); // 释放座位或标记为已售 seatService.updateSeatsStatus( order.getSeatIds(), status == PaymentStatus.SUCCESS ? SeatStatus.SOLD : SeatStatus.AVAILABLE); } finally { redisTemplate.delete(lockKey); } }这套影院订票系统涵盖了企业级应用开发的典型技术组合,在实际开发中还需要根据具体业务需求持续优化。我在实现过程中特别体会到:对于高并发场景,除了技术方案的选择外,合理的重试机制、完善的监控告警、清晰的日志记录同样重要。建议在开发初期就建立全链路日志追踪体系,这对后期排查线上问题会有极大帮助。