1. 项目概述与设计思路
在线电影购票系统作为典型的电子商务应用,其核心目标是解决传统影院购票排队时间长、场次信息获取不便等问题。我们采用前后端分离架构设计,后端基于Spring Boot框架实现RESTful API服务,前端使用Vue.js构建响应式用户界面,数据库选用MySQL 8.0存储业务数据。这种架构选择主要基于以下考量:
技术选型理由:
- Spring Boot的自动配置特性大幅减少了传统Spring MVC的XML配置工作量,内嵌Tomcat服务器简化了部署流程,起步依赖(Starter)机制确保各组件版本兼容性
- Vue.js的组件化开发模式与虚拟DOM技术特别适合构建动态交互频繁的购票界面,其响应式数据绑定可实时更新座位选择状态
- MySQL的事务支持能有效处理高并发下的票务冲突,通过行级锁和乐观锁机制保证座位数据的准确性
提示:在电商类系统中,库存(座位)的并发控制是核心难点,本系统采用SELECT FOR UPDATE实现悲观锁,配合Redis缓存减轻数据库压力
系统采用经典的三层架构:
- 表现层:Vue.js构建的SPA应用,通过Axios与后端交互
- 业务逻辑层:Spring Boot实现的RESTful服务,包含用户认证、订单处理等核心模块
- 数据访问层:MyBatis-Plus操作MySQL,Redis缓存热点数据
2. 核心功能模块实现
2.1 用户认证与权限管理
采用JWT+Spring Security实现安全的认证体系。关键实现要点:
java复制// JWT生成过滤器核心代码
public class JwtAuthenticationFilter extends OncePerRequestFilter {
@Override
protected void doFilterInternal(HttpServletRequest request,
HttpServletResponse response,
FilterChain filterChain) throws ServletException {
String token = request.getHeader("Authorization");
if (token != null && token.startsWith("Bearer ")) {
String username = jwtUtil.extractUsername(token.substring(7));
if (username != null && SecurityContextHolder.getContext().getAuthentication() == null) {
UserDetails userDetails = userDetailsService.loadUserByUsername(username);
if (jwtUtil.validateToken(token.substring(7), userDetails)) {
UsernamePasswordAuthenticationToken authToken = new UsernamePasswordAuthenticationToken(
userDetails, null, userDetails.getAuthorities());
authToken.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
SecurityContextHolder.getContext().setAuthentication(authToken);
}
}
}
filterChain.doFilter(request, response);
}
}
权限控制采用RBAC模型,数据库设计包含五张核心表:
- 用户表(t_user):存储登录凭证和基本信息
- 角色表(t_role):定义角色类型(管理员、普通用户等)
- 权限表(t_permission):细粒度权限项(如"电影管理:新增")
- 用户角色关联表(t_user_role)
- 角色权限关联表(t_role_permission)
2.2 电影场次与座位管理
场次信息采用时空二维设计:
sql复制CREATE TABLE `t_schedule` (
`id` bigint NOT NULL AUTO_INCREMENT,
`movie_id` bigint NOT NULL COMMENT '电影ID',
`cinema_id` bigint NOT NULL COMMENT '影院ID',
`hall_id` bigint NOT NULL COMMENT '影厅ID',
`start_time` datetime NOT NULL COMMENT '开场时间',
`end_time` datetime NOT NULL COMMENT '结束时间',
`price` decimal(10,2) NOT NULL 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;
CREATE TABLE `t_seat` (
`id` bigint NOT NULL AUTO_INCREMENT,
`schedule_id` bigint NOT NULL COMMENT '场次ID',
`row_num` int NOT NULL COMMENT '行号',
`col_num` int NOT NULL COMMENT '列号',
`status` tinyint NOT NULL DEFAULT '0' COMMENT '状态(0可选 1已售 2锁定)',
`lock_time` datetime DEFAULT NULL COMMENT '锁定时间',
PRIMARY KEY (`id`),
UNIQUE KEY `uk_schedule_seat` (`schedule_id`,`row_num`,`col_num`),
KEY `idx_status` (`status`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
座位选择采用WebSocket实现实时同步:
javascript复制// Vue组件中的座位选择逻辑
methods: {
selectSeat(seat) {
if (seat.status !== 0) return;
this.$socket.send(JSON.stringify({
type: 'lock',
scheduleId: this.scheduleId,
seatId: seat.id
}));
this.$store.commit('addSelectedSeat', seat);
}
}
2.3 订单支付流程
支付状态机设计:
mermaid复制stateDiagram
[*] --> 待支付
待支付 --> 已取消: 超时未支付(30分钟)
待支付 --> 已支付: 支付成功
已支付 --> 已完成: 观影时间过后
已支付 --> 已退款: 用户申请退款
支付接口防重处理:
java复制@Transactional
public String createOrder(OrderDTO dto) {
// 幂等性检查
if (redisTemplate.opsForValue().get("order:token:" + dto.getToken()) != null) {
throw new BusinessException("请勿重复提交订单");
}
// 库存预扣减
List<Seat> seats = seatService.lockSeats(dto.getScheduleId(), dto.getSeatIds());
if (seats.size() != dto.getSeatIds().size()) {
throw new BusinessException("部分座位已被预订");
}
// 订单入库
Order order = new Order();
BeanUtils.copyProperties(dto, order);
order.setOrderNo(generateOrderNo());
order.setStatus(OrderStatus.UNPAID.getCode());
orderMapper.insert(order);
// 设置防重token
redisTemplate.opsForValue().set("order:token:" + dto.getToken(), "1", 30, TimeUnit.MINUTES);
return order.getOrderNo();
}
3. 性能优化实践
3.1 缓存策略设计
采用多级缓存架构提升系统响应速度:
- 前端缓存:Vuex管理应用状态,localStorage持久化用户偏好
- 网关缓存:Nginx对静态资源和API响应进行缓存
- 服务端缓存:
- Redis缓存热点数据(电影详情、场次信息)
- Caffeine本地缓存用户权限数据
缓存更新策略:
java复制@CacheEvict(value = "movie", key = "#movie.id")
public void updateMovie(Movie movie) {
movieMapper.updateById(movie);
// 异步更新搜索索引
searchService.updateMovieIndex(movie);
}
3.2 数据库优化方案
- 索引优化:
- 为所有外键字段添加索引
- 为高频查询条件(如status, create_time)添加组合索引
- 查询优化:
- 使用MyBatis-Plus的QueryWrapper避免N+1查询
- 大数据量分页采用"先查ID再关联"模式
- 连接池配置:
yaml复制spring: datasource: hikari: maximum-pool-size: 20 minimum-idle: 5 connection-timeout: 30000 idle-timeout: 600000 max-lifetime: 1800000
3.3 高并发应对措施
- 购票环节采用分布式锁:
java复制public boolean lockSeats(Long scheduleId, List<Long> seatIds) {
String lockKey = "schedule:lock:" + scheduleId;
String requestId = UUID.randomUUID().toString();
try {
// 尝试获取分布式锁
Boolean locked = redisTemplate.opsForValue().setIfAbsent(
lockKey, requestId, 10, TimeUnit.SECONDS);
if (Boolean.TRUE.equals(locked)) {
// 执行座位锁定逻辑
return seatMapper.updateSeatStatus(scheduleId, seatIds, 0, 2) > 0;
}
return false;
} finally {
// 释放锁时要验证requestId避免误删
if (requestId.equals(redisTemplate.opsForValue().get(lockKey))) {
redisTemplate.delete(lockKey);
}
}
}
- 秒杀场景采用库存预热+异步扣减:
java复制@RabbitListener(queues = "order.queue")
public void processOrder(OrderMessage message) {
// 1. 检查库存
int stock = redisTemplate.opsForValue().decrement("stock:" + message.getGoodsId());
if (stock < 0) {
redisTemplate.opsForValue().increment("stock:" + message.getGoodsId());
return;
}
// 2. 创建订单
createOrderInDB(message);
}
4. 安全防护体系
4.1 常见攻击防护
- SQL注入防护:
- 全程使用MyBatis参数化查询
- 禁止拼接SQL语句
- XSS防护:
- 前端使用vue-sanitize过滤富文本
- 后端采用Jackson的@JsonSerialize转义HTML
- CSRF防护:
- 启用Spring Security的CSRF保护
- 敏感操作要求二次验证
4.2 敏感数据保护
- 密码存储:
java复制@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder(12);
}
- 数据脱敏:
java复制public String desensitizePhone(String phone) {
if (StringUtils.isEmpty(phone) || phone.length() != 11) {
return phone;
}
return phone.substring(0, 3) + "****" + phone.substring(7);
}
- 日志过滤:
java复制@Bean
public FilterRegistrationBean<LogFilter> logFilter() {
FilterRegistrationBean<LogFilter> registration = new FilterRegistrationBean<>();
registration.setFilter(new LogFilter());
registration.addUrlPatterns("/*");
registration.setOrder(Ordered.HIGHEST_PRECEDENCE);
return registration;
}
5. 部署与监控方案
5.1 容器化部署
Docker Compose编排文件示例:
yaml复制version: '3.8'
services:
mysql:
image: mysql:8.0
environment:
MYSQL_ROOT_PASSWORD: ${DB_PASSWORD}
volumes:
- mysql_data:/var/lib/mysql
ports:
- "3306:3306"
redis:
image: redis:6.2
ports:
- "6379:6379"
backend:
build: ./backend
ports:
- "8080:8080"
depends_on:
- mysql
- redis
frontend:
build: ./frontend
ports:
- "80:80"
volumes:
mysql_data:
5.2 监控告警配置
- Spring Boot Actuator健康检查:
yaml复制management:
endpoints:
web:
exposure:
include: "*"
endpoint:
health:
show-details: always
- Prometheus监控指标:
java复制@Bean
public MeterRegistryCustomizer<PrometheusMeterRegistry> metricsCommonTags() {
return registry -> registry.config().commonTags("application", "movie-ticket");
}
- ELK日志收集:
xml复制<dependency>
<groupId>net.logstash.logback</groupId>
<artifactId>logstash-logback-encoder</artifactId>
<version>6.6</version>
</dependency>
6. 项目演进方向
- 推荐系统集成:
- 基于用户历史行为实现协同过滤推荐
- 结合电影特征实现内容相似度推荐
- 大数据分析:
- 使用Flink实时分析购票趋势
- 生成影院热力图辅助排片决策
- 微服务改造:
- 按业务域拆分为电影服务、订单服务等
- 引入Spring Cloud Alibaba生态
- 多端适配:
- 开发微信小程序版本
- 适配影院自助取票机
在实际开发过程中,我们发现影院排片数据的实时同步是个关键挑战。最终采用的解决方案是通过数据库变更捕获(CDC)技术,使用Debezium将MySQL的binlog转换为Kafka消息,再由各个子系统消费更新自己的数据副本。这种方案既保证了数据一致性,又将各模块解耦。