1. 项目背景与核心需求
在线电影票购买系统是当前互联网+娱乐产业中最典型的应用场景之一。作为一个完整的JavaWeb项目,它涵盖了用户管理、影片信息展示、场次排期、座位选择、订单支付等核心业务流程。这类系统在实际开发中需要兼顾高并发访问、实时数据更新和良好的用户体验。
从技术实现角度来看,一个健壮的在线票务系统需要解决以下几个关键问题:
- 如何设计合理的数据库结构来存储影片、影院、场次等多维关联数据
- 如何实现高并发的座位锁定机制避免超卖
- 如何设计前后端交互流程保证购票过程的顺畅性
- 如何集成第三方支付接口完成交易闭环
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 系统架构设计
2.1 技术栈选型
基于JavaWeb技术栈的典型组合方案:
- 后端框架:Spring Boot 2.7 + MyBatis Plus
- 前端技术:Thymeleaf模板引擎 + Bootstrap 5
- 数据库:MySQL 8.0 + Redis 6.2
- 其他组件:Lombok、Hutool工具包、PageHelper分页插件
选择Spring Boot而非传统SSM框架的主要考虑:
- 自动配置特性大幅减少XML配置工作量
- 内嵌Tomcat简化部署流程
- 丰富的Starter依赖可快速集成常用功能
- Actuator端点便于生产环境监控
2.2 系统分层架构
采用经典的三层架构设计:
code复制表示层(Web)
↓
业务逻辑层(Service)
↓
数据访问层(Dao)
每层的具体职责:
- 表示层:处理HTTP请求、参数校验、视图渲染
- 业务层:核心业务逻辑实现、事务控制
- 数据层:数据库CRUD操作、缓存处理
3. 数据库设计与实现
3.1 核心表结构
sql复制-- 影片信息表
CREATE TABLE `movie` (
`id` bigint PRIMARY KEY AUTO_INCREMENT,
`title` varchar(100) NOT NULL COMMENT '影片名称',
`cover_url` varchar(255) COMMENT '封面图URL',
`duration` int COMMENT '时长(分钟)',
`release_date` date COMMENT '上映日期',
`score` decimal(3,1) COMMENT '评分',
`description` text COMMENT '剧情简介'
);
-- 影院信息表
CREATE TABLE `cinema` (
`id` bigint PRIMARY KEY AUTO_INCREMENT,
`name` varchar(100) NOT NULL,
`address` varchar(255),
`contact_phone` varchar(20)
);
-- 放映厅表
CREATE TABLE `hall` (
`id` bigint PRIMARY KEY AUTO_INCREMENT,
`cinema_id` bigint NOT NULL,
`name` varchar(50) NOT NULL,
`seat_layout` text COMMENT '座位布局JSON',
FOREIGN KEY (`cinema_id`) REFERENCES `cinema`(`id`)
);
-- 场次表
CREATE TABLE `schedule` (
`id` bigint PRIMARY KEY AUTO_INCREMENT,
`movie_id` bigint NOT NULL,
`hall_id` bigint NOT NULL,
`start_time` datetime NOT NULL,
`price` decimal(10,2) NOT NULL,
`remaining_seats` int NOT NULL,
FOREIGN KEY (`movie_id`) REFERENCES `movie`(`id`),
FOREIGN KEY (`hall_id`) REFERENCES `hall`(`id`)
);
-- 订单表
CREATE TABLE `order` (
`id` bigint PRIMARY KEY AUTO_INCREMENT,
`user_id` bigint NOT NULL,
`schedule_id` bigint NOT NULL,
`seats` varchar(255) NOT NULL COMMENT '座位号,多个用逗号分隔',
`total_amount` decimal(10,2) NOT NULL,
`status` tinyint NOT NULL COMMENT '0-待支付 1-已支付 2-已取消',
`create_time` datetime NOT NULL,
FOREIGN KEY (`schedule_id`) REFERENCES `schedule`(`id`)
);
3.2 索引优化策略
为提高查询性能,针对高频查询字段建立索引:
sql复制-- 场次表的复合索引
CREATE INDEX idx_schedule_movie ON `schedule`(`movie_id`, `start_time`);
CREATE INDEX idx_schedule_cinema ON `schedule`(`hall_id`, `start_time`);
-- 订单表的查询索引
CREATE INDEX idx_order_user ON `order`(`user_id`, `create_time`);
CREATE INDEX idx_order_schedule ON `order`(`schedule_id`);
4. 核心功能实现
4.1 座位锁定机制
解决并发购票时的座位冲突问题,采用Redis分布式锁+数据库乐观锁的双重保障:
java复制public boolean lockSeats(Long scheduleId, String seatNumbers) {
String lockKey = "seat_lock:" + scheduleId;
// 获取分布式锁
String requestId = UUID.randomUUID().toString();
try {
boolean locked = redisTemplate.opsForValue().setIfAbsent(
lockKey, requestId, 30, TimeUnit.SECONDS);
if (!locked) {
return false;
}
// 查询场次信息
Schedule schedule = scheduleMapper.selectById(scheduleId);
if (schedule.getRemainingSeats() <= 0) {
return false;
}
// 检查座位是否可用
List<String> seats = Arrays.asList(seatNumbers.split(","));
List<Order> existingOrders = orderMapper.selectByScheduleAndSeats(
scheduleId, seats);
if (!existingOrders.isEmpty()) {
return false;
}
// 更新剩余座位数(乐观锁)
int rows = scheduleMapper.updateRemainingSeats(
scheduleId, schedule.getRemainingSeats() - seats.size(),
schedule.getRemainingSeats());
return rows > 0;
} finally {
// 释放锁
if (requestId.equals(redisTemplate.opsForValue().get(lockKey))) {
redisTemplate.delete(lockKey);
}
}
}
4.2 订单超时处理
使用Spring的@Scheduled注解实现定时任务,自动取消未支付订单:
java复制@Scheduled(cron = "0 */5 * * * ?")
public void cancelUnpaidOrders() {
LocalDateTime threshold = LocalDateTime.now().minusMinutes(15);
List<Order> unpaidOrders = orderMapper.selectUnpaidBefore(threshold);
unpaidOrders.forEach(order -> {
order.setStatus(OrderStatus.CANCELLED.getCode());
orderMapper.updateById(order);
// 恢复座位库存
scheduleMapper.increaseRemainingSeats(
order.getScheduleId(),
order.getSeats().split(",").length);
});
}
5. 前端页面实现要点
5.1 座位选择交互
使用JavaScript实现可视化选座功能:
javascript复制function initSeatMap(layoutJson) {
const seats = JSON.parse(layoutJson);
const container = document.getElementById('seat-container');
seats.forEach(row => {
const rowDiv = document.createElement('div');
rowDiv.className = 'seat-row';
row.seats.forEach(seat => {
const seatEl = document.createElement('div');
seatEl.className = `seat ${seat.status}`;
seatEl.dataset.seatId = seat.id;
seatEl.addEventListener('click', () => toggleSeatSelection(seat));
rowDiv.appendChild(seatEl);
});
container.appendChild(rowDiv);
});
}
function toggleSeatSelection(seat) {
if (seat.status === 'available') {
seat.status = 'selected';
selectedSeats.push(seat.id);
} else if (seat.status === 'selected') {
seat.status = 'available';
selectedSeats = selectedSeats.filter(id => id !== seat.id);
}
updateSeatDisplay();
}
5.2 支付流程实现
集成支付宝沙箱环境的支付示例:
java复制@PostMapping("/pay")
public String createPayment(@RequestParam Long orderId,
HttpServletResponse response) throws Exception {
Order order = orderService.getById(orderId);
if (order == null || order.getStatus() != 0) {
throw new BusinessException("订单状态异常");
}
AlipayClient alipayClient = new DefaultAlipayClient(
"https://openapi.alipaydev.com/gateway.do",
APP_ID,
APP_PRIVATE_KEY,
"json",
"UTF-8",
ALIPAY_PUBLIC_KEY,
"RSA2");
AlipayTradePagePayRequest request = new AlipayTradePagePayRequest();
request.setReturnUrl("http://yourdomain.com/order/pay/success");
request.setNotifyUrl("http://yourdomain.com/api/pay/notify");
JSONObject bizContent = new JSONObject();
bizContent.put("out_trade_no", order.getId().toString());
bizContent.put("total_amount", order.getTotalAmount());
bizContent.put("subject", "电影票订单-" + order.getId());
bizContent.put("product_code", "FAST_INSTANT_TRADE_PAY");
request.setBizContent(bizContent.toString());
String form = alipayClient.pageExecute(request).getBody();
response.setContentType("text/html;charset=UTF-8");
response.getWriter().write(form);
return null;
}
6. 系统部署与性能优化
6.1 生产环境部署方案
推荐使用Docker Compose编排服务:
yaml复制version: '3'
services:
mysql:
image: mysql:8.0
environment:
MYSQL_ROOT_PASSWORD: yourpassword
MYSQL_DATABASE: movie_ticket
ports:
- "3306:3306"
volumes:
- ./mysql-data:/var/lib/mysql
redis:
image: redis:6.2
ports:
- "6379:6379"
volumes:
- ./redis-data:/data
app:
build: .
ports:
- "8080:8080"
depends_on:
- mysql
- redis
environment:
SPRING_DATASOURCE_URL: jdbc:mysql://mysql:3306/movie_ticket
SPRING_REDIS_HOST: redis
6.2 性能优化建议
-
缓存策略:
- 影片列表、影院信息等静态数据使用Redis缓存
- 设置合理的过期时间(如影片信息缓存1小时)
- 使用@Cacheable注解简化缓存逻辑
-
数据库优化:
- 配置连接池参数(HikariCP推荐配置)
- 对大表进行分表处理(如订单表按月份分表)
- 定期执行ANALYZE TABLE更新统计信息
-
前端优化:
- 使用CDN加速静态资源加载
- 实现图片懒加载
- 对API响应进行Gzip压缩
7. 常见问题与解决方案
7.1 座位超卖问题
现象:多个用户同时选择同一座位导致数据不一致
解决方案:
- 前端限制:选座时实时查询座位状态并禁用已选座位
- 后端校验:创建订单前再次验证座位可用性
- 数据库锁:使用SELECT FOR UPDATE锁定相关记录
- 最终保障:支付完成后再次确认座位状态
7.2 支付回调处理
支付成功但订单状态未更新的可能原因:
- 网络问题导致支付宝回调未到达
- 回调接口处理异常
- 验签失败
应对措施:
- 实现主动查询机制:定时检查未支付订单的支付状态
- 增加回调日志记录便于排查
- 设置合理的订单状态超时时间
java复制@PostMapping("/pay/notify")
public String handlePayNotify(@RequestParam Map<String,String> params) {
// 验证签名
boolean signVerified = AlipaySignature.rsaCheckV1(
params, ALIPAY_PUBLIC_KEY, "UTF-8", "RSA2");
if (!signVerified) {
return "failure";
}
String tradeStatus = params.get("trade_status");
if ("TRADE_SUCCESS".equals(tradeStatus)) {
String orderNo = params.get("out_trade_no");
Order order = orderService.getById(Long.parseLong(orderNo));
if (order != null && order.getStatus() == 0) {
order.setStatus(1);
orderService.updateById(order);
}
}
return "success";
}
8. 项目扩展方向
-
会员积分系统:
- 购票获得积分
- 积分兑换优惠券
- 会员等级特权
-
推荐算法:
- 基于用户历史行为的影片推荐
- 热门影片排行榜
- 相似用户偏好分析
-
多影院管理:
- 影院后台管理系统
- 场次排期管理
- 销售数据统计
-
移动端适配:
- 开发微信小程序版本
- 响应式设计适配手机浏览器
- APP推送通知
在实际开发这类系统时,有几个关键点需要特别注意:首先是座位锁定机制的可靠性,这直接关系到系统的核心功能是否正常;其次是支付流程的完整性,需要处理好各种异常情况;最后是性能优化,特别是在热门影片开售时可能会出现的瞬时高并发场景。
