1. 项目背景与核心需求
电影票务系统从传统柜台销售向线上迁移已成必然趋势。去年国内线上购票占比已达87%,但市面上多数系统仍存在界面陈旧、选座体验差、支付流程繁琐等问题。这个基于SpringBoot的在线购票系统正是针对这些痛点设计的现代化解决方案。
核心要解决三个关键问题:
- 影院排片与座位状态的实时同步
- 高并发场景下的票务库存管理
- 用户友好的可视化交互界面
我选择SpringBoot 1.6.9版本作为基础框架,主要考虑其稳定的自动配置机制和与Thymeleaf模板引擎的良好整合性。这个版本虽不是最新,但在中小型系统构建中仍保持着良好的性能和兼容性平衡。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 系统架构设计
2.1 技术栈选型
后端核心组件:
- Spring Boot 1.6.9(Web+Security+JPA)
- MySQL 5.7(事务隔离级别RR)
- Redis 3.2(缓存影院座位状态)
- Quartz 2.3.0(定时同步排片)
前端可视化方案:
- Thymeleaf 3.0 + Bootstrap 4
- ECharts 4.2(数据统计展示)
- jQuery Seat Charts 2.0(影院座位图)
特别提示:SpringBoot 1.x与2.x的自动配置机制有显著差异,1.6.9版本需要手动配置Jackson的日期序列化格式,否则LocalDateTime会输出为数组形式。
2.2 数据库关键表设计
sql复制CREATE TABLE `schedule` (
`id` bigint(20) NOT NULL AUTO_INCREMENT,
`movie_id` bigint(20) NOT NULL COMMENT '影片ID',
`cinema_id` int(11) NOT NULL COMMENT '影院ID',
`hall_id` int(11) NOT NULL COMMENT '影厅ID',
`show_time` datetime NOT NULL COMMENT '放映时间',
`price` decimal(10,2) NOT NULL DEFAULT '0.00',
`seat_status` longtext COMMENT '座位状态JSON',
`version` int(11) NOT NULL DEFAULT '0' COMMENT '乐观锁版本号',
PRIMARY KEY (`id`),
KEY `idx_movie` (`movie_id`),
KEY `idx_time` (`show_time`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
座位状态字段采用JSON格式存储影厅座位图,例如:
json复制{
"A1": {"type":"normal","status":"available"},
"A2": {"type":"couple","status":"locked"},
"B5": {"type":"disabled","status":"unavailable"}
}
3. 核心功能实现细节
3.1 选座可视化交互
前端使用jQuery Seat Charts渲染座位图时,需要处理三种状态变化:
- 用户点击选中(前端即时响应)
- 座位状态校验(与Redis实时同步)
- 最终提交时的并发控制(MySQL乐观锁)
典型的问题场景:用户A看到座位空闲并选择,此时用户B已完成该座位支付。解决方案是采用Redis原子操作:
java复制// 座位预占逻辑
public boolean tryLockSeat(Long scheduleId, String seatNo) {
String key = "schedule:" + scheduleId + ":seats";
return redisTemplate.opsForHash().putIfAbsent(key, seatNo, "locking") == Boolean.TRUE;
}
3.2 支付流程设计
支付状态机实现要点:
mermaid复制stateDiagram-v2
[*] --> 待支付: 创建订单
待支付 --> 已取消: 超时未支付
待支付 --> 支付中: 用户发起支付
支付中 --> 已取消: 支付失败
支付中 --> 已完成: 支付成功
已完成 --> 已退款: 用户申请退款
实际代码中需要处理微信/支付宝的异步通知,特别注意幂等性控制:
java复制@Transactional
public void handlePayNotify(String orderNo, String paymentType) {
Order order = orderRepository.findByOrderNoForUpdate(orderNo);
if (order.getStatus() != OrderStatus.PAYING) {
log.warn("订单状态异常: {}", order.getStatus());
return;
}
// 更新订单状态
order.setStatus(OrderStatus.PAID);
// 同步更新排期座位状态
scheduleService.confirmSeats(order.getScheduleId(), order.getSeats());
}
4. 可视化数据分析模块
4.1 影院热力图实现
使用ECharts的热力图组件展示各时段上座率:
javascript复制function renderHeatMap(data) {
const option = {
tooltip: {},
visualMap: {
min: 0,
max: 100,
calculable: true,
inRange: {
color: ['#313695', '#4575b4', '#74add1', '#abd9e9', '#e0f3f8', '#ffffbf', '#fee090', '#fdae61', '#f46d43', '#d73027', '#a50026']
}
},
calendar: {
range: '2023-10'
},
series: [{
type: 'heatmap',
coordinateSystem: 'calendar',
data: data.map(item => [
item.date,
item.timeSlot,
item.occupancyRate
])
}]
};
heatmapChart.setOption(option);
}
4.2 实时票房统计
采用WebSocket推送实时数据:
java复制@GetMapping("/stats/boxoffice")
public String getBoxOfficeStats(@RequestParam String date) {
// 使用缓存降低数据库压力
String cacheKey = "stats:boxoffice:" + date;
String cached = redisTemplate.opsForValue().get(cacheKey);
if (cached != null) {
return cached;
}
List<BoxOfficeDTO> data = movieService.getDailyBoxOffice(date);
String json = objectMapper.writeValueAsString(data);
redisTemplate.opsForValue().set(cacheKey, json, 5, TimeUnit.MINUTES);
return json;
}
5. 性能优化实践
5.1 缓存策略设计
三级缓存体系:
- 本地缓存(Caffeine):存储静态数据如影院信息
java复制@Bean public CacheManager cacheManager() { CaffeineCacheManager manager = new CaffeineCacheManager(); manager.setCaffeine(Caffeine.newBuilder() .expireAfterWrite(30, TimeUnit.MINUTES) .maximumSize(1000)); return manager; } - Redis缓存:存储动态数据如座位状态
- MySQL:持久化存储
5.2 高并发场景应对
模拟500并发测试时发现的典型问题及解决方案:
| 问题现象 | 根本原因 | 解决方案 |
|---|---|---|
| 座位超卖 | MySQL更新延迟 | Redis分布式锁+版本号控制 |
| 支付重复通知 | 网络重传 | 支付流水表唯一索引 |
| 统计不准确 | 缓存雪崩 | 多级缓存+随机过期时间 |
压测关键指标优化前后对比:
text复制优化前:
- 平均响应时间:1200ms
- TPS:85
- 错误率:8.7%
优化后:
- 平均响应时间:380ms
- TPS:210
- 错误率:0.3%
6. 部署与监控
6.1 生产环境配置
推荐使用Docker Compose部署:
yaml复制version: '3'
services:
app:
image: openjdk:8-jre
ports:
- "8080:8080"
volumes:
- ./app.jar:/app.jar
command: ["java", "-jar", "/app.jar"]
depends_on:
- redis
- mysql
redis:
image: redis:3.2-alpine
ports:
- "6379:6379"
volumes:
- redis_data:/data
mysql:
image: mysql:5.7
environment:
MYSQL_ROOT_PASSWORD: yourpassword
ports:
- "3306:3306"
volumes:
- mysql_data:/var/lib/mysql
volumes:
redis_data:
mysql_data:
6.2 监控方案
- Spring Boot Actuator暴露关键指标
properties复制management.endpoints.web.exposure.include=health,info,metrics management.metrics.export.prometheus.enabled=true - Grafana监控看板配置示例:
- JVM内存使用率
- 接口QPS/RT
- 缓存命中率
- 活跃会话数
7. 典型问题排查记录
7.1 座位状态不同步问题
现象:用户偶尔看到座位已被占但界面仍显示可用
排查过程:
- 检查Redis集群状态 - 正常
- 查看网络延迟 - <2ms
- 最终发现是前端轮询间隔(5s)与后端缓存过期时间(3s)不匹配
解决方案:
javascript复制// 调整轮询逻辑
function pollSeatStatus() {
fetchSeatData().then(updateUI);
setTimeout(pollSeatStatus, 2000); // 改为2秒
}
7.2 支付回调丢失问题
关键日志分析:
code复制2023-03-12 11:23:45 [http-nio-8080-exec-7] INFO 收到支付通知:orderNo=202303120012
2023-03-12 11:23:46 [http-nio-8080-exec-7] ERROR 更新订单状态失败:Deadlock found
解决方案:
- 增加支付回调日志表
- 实现补偿任务扫描超时订单
- 优化事务隔离级别为RC
8. 扩展功能建议
-
智能推荐系统:
- 基于用户历史购票记录推荐影片
- 协同过滤算法实现
-
动态定价策略:
java复制public BigDecimal calculateDynamicPrice(LocalDateTime showTime, float occupancyRate) { float basePrice = getBasePrice(); // 开场前2小时且上座率<30%时降价 if (ChronoUnit.HOURS.between(LocalDateTime.now(), showTime) < 2 && occupancyRate < 0.3f) { return basePrice.multiply(new BigDecimal("0.8")); } return basePrice; } -
移动端优化:
- 实现PWA应用
- 添加小程序接入
这个项目从设计到上线历时三个月,最大的收获是认识到可视化系统必须保持前后端状态的高度一致性。特别是在座位选择这种强交互场景下,任何微小的延迟都会导致用户体验下降。建议后来者在类似项目中,一定要把状态同步机制作为核心架构问题来设计。
