1. 项目背景与需求分析
洋州影院作为一家地方性影院,面临着传统人工售票模式带来的诸多痛点:售票窗口排长队、座位信息更新不及时、财务统计困难等问题日益凸显。我在实际调研中发现,超过70%的观众更倾向于通过手机提前选座购票,而现有的线下售票系统根本无法满足这一需求。
这个基于SpringBoot+Vue的购票管理系统正是为解决这些问题而设计的。系统需要实现的核心功能包括:
- 用户端:影片浏览、在线选座、电子支付、订单管理
- 管理端:影片管理、排片管理、销售统计、用户管理
特别值得注意的是,系统需要处理高并发的座位锁定问题——当多个用户同时选择同一座位时,系统必须保证数据的一致性。这在实际运营中是个常见痛点,也是我们技术方案需要重点解决的问题。
2. 技术选型与架构设计
2.1 前后端分离架构
我们采用前后端分离的设计模式,这种架构在现代Web开发中已经成为主流选择。具体分工如下:
后端技术栈:
- Spring Boot 2.7.x:提供RESTful API接口
- MyBatis-Plus 3.5.x:简化数据库操作
- MySQL 8.0:关系型数据库存储
- Redis 6.x:处理座位锁等高并发场景
前端技术栈:
- Vue 3.x:前端框架
- Element Plus:UI组件库
- Axios:HTTP请求处理
- Vue Router:前端路由管理
提示:选择MyBatis-Plus而非JPA是考虑到项目需要更灵活的SQL控制,特别是在复杂报表查询场景下。
2.2 数据库设计优化
在数据库设计阶段,我们特别注意了以下几个关键点:
-
索引优化:在user_info表的user_email和user_phone字段上建立唯一索引,既保证查询效率又防止重复注册。
-
字段类型选择:
- 金额字段使用DECIMAL(10,2)而非FLOAT,避免浮点数精度问题
- 时间字段统一使用datetime类型,而非timestamp,避免时区转换问题
-
关联设计:
sql复制ALTER TABLE order_info ADD CONSTRAINT fk_order_user
FOREIGN KEY (user_id) REFERENCES user_info(user_id) ON DELETE CASCADE;
这种外键约束确保了数据完整性,ON DELETE CASCADE设置使得删除用户时自动清理其订单记录。
3. 核心功能实现细节
3.1 在线选座功能实现
选座功能是系统的核心难点,我们采用以下方案解决并发问题:
- 座位锁定机制:
java复制// 使用Redis分布式锁
public boolean lockSeats(Long scheduleId, List<String> seatNos) {
String lockKey = "lock:schedule:" + 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;
}
- 状态机设计:
- 座位状态:0=可用,1=已锁定,2=已售出
- 订单状态:0=待支付,1=已支付,2=已取消
3.2 支付流程设计
支付流程采用了状态模式来管理订单生命周期:
java复制public class OrderService {
private OrderState state;
public void setState(OrderState state) {
this.state = state;
}
public void handlePayment(Long orderId) {
state.handlePayment(this, orderId);
}
public void handleTimeout(Long orderId) {
state.handleTimeout(this, orderId);
}
}
// 具体状态实现
public class PendingPaymentState implements OrderState {
@Override
public void handlePayment(OrderService service, Long orderId) {
// 更新订单状态为已支付
// 更新座位状态为已售出
service.setState(new CompletedState());
}
}
4. 性能优化实践
4.1 缓存策略
我们采用多级缓存策略提升系统响应速度:
- Redis缓存:
- 影片信息缓存:60分钟过期
- 热门场次缓存:30分钟过期
- 使用Hash结构存储对象,节省内存空间
- 本地缓存:
- 使用Caffeine缓存配置信息
- 最大数量1000条,10分钟过期
4.2 SQL优化案例
在票房统计查询中,我们优化了如下SQL:
优化前:
sql复制SELECT m.movie_name, COUNT(o.order_id)
FROM movie_info m
LEFT JOIN schedule_info s ON m.movie_id = s.movie_id
LEFT JOIN order_info o ON s.schedule_id = o.schedule_id
GROUP BY m.movie_id;
优化后:
sql复制SELECT m.movie_name, t.order_count
FROM movie_info m
JOIN (
SELECT s.movie_id, COUNT(o.order_id) as order_count
FROM schedule_info s
JOIN order_info o ON s.schedule_id = o.schedule_id
WHERE o.order_status = 1
GROUP BY s.movie_id
) t ON m.movie_id = t.movie_id;
优化后查询时间从1200ms降低到350ms,性能提升65%。
5. 安全防护措施
5.1 认证与授权
采用JWT进行身份认证,关键配置如下:
java复制@Configuration
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity http) throws Exception {
http.csrf().disable()
.authorizeRequests()
.antMatchers("/api/auth/**").permitAll()
.antMatchers("/api/admin/**").hasRole("ADMIN")
.anyRequest().authenticated()
.and()
.addFilter(new JwtAuthenticationFilter(authenticationManager()))
.addFilter(new JwtAuthorizationFilter(authenticationManager()));
}
}
5.2 敏感数据保护
- 密码存储:使用BCryptPasswordEncoder加密
java复制@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
- 日志脱敏:自定义Logback过滤器
xml复制<filter class="com.yangzhou.util.SensitiveDataFilter">
<patterns>
<pattern>password\":\s*"([^"]*)"</pattern>
<pattern>phone\":\s*"(\d{3})\d{4}(\d{4})"</pattern>
</patterns>
</filter>
6. 部署与监控
6.1 容器化部署
使用Docker Compose编排服务:
yaml复制version: '3'
services:
mysql:
image: mysql:8.0
environment:
MYSQL_ROOT_PASSWORD: ${DB_PASSWORD}
volumes:
- ./mysql/data:/var/lib/mysql
redis:
image: redis:6.2
ports:
- "6379:6379"
backend:
build: ./backend
ports:
- "8080:8080"
depends_on:
- mysql
- redis
6.2 监控方案
- Spring Boot Actuator健康检查
- Prometheus + Grafana监控面板
- ELK日志收集系统
关键监控指标:
- API响应时间P99 < 500ms
- 错误率 < 0.5%
- JVM内存使用率 < 70%
7. 踩坑经验分享
在实际开发中,我们遇到了几个典型问题:
-
座位并发问题:
初始方案使用数据库乐观锁,但在高并发测试时出现了超卖现象。最终采用Redis分布式锁+数据库事务的方案解决。 -
JWT续签问题:
前端在token过期前5分钟自动调用刷新接口,避免用户操作中断。 -
Vue组件复用问题:
相同路由参数跳转时组件不刷新的问题,通过监听路由变化解决:
javascript复制watch: {
'$route'(to, from) {
if(to.path === from.path) {
this.loadData()
}
}
}
- MyBatis结果映射:
复杂查询结果使用ResultMap解决N+1查询问题:
xml复制<resultMap id="scheduleDetailMap" type="ScheduleDTO">
<id property="scheduleId" column="schedule_id"/>
<association property="movie" javaType="Movie">
<id property="movieId" column="movie_id"/>
<result property="movieName" column="movie_name"/>
</association>
</resultMap>
这个项目从技术选型到最终上线历时3个月,最大的收获是深入理解了分布式系统的一致性问题解决方案。特别是在座位锁定场景下,如何平衡系统性能和数据一致性是个值得持续优化的课题。对于准备开发类似系统的同学,建议前期充分做好压力测试,模拟真实的高并发场景,这能帮助发现很多潜在问题。