1. 项目背景与核心价值
电影院选票系统是典型的Java Web应用场景,这个毕设选题之所以经典,是因为它完整覆盖了企业级开发的三大核心要素:高并发事务处理(座位锁定)、复杂业务逻辑(排片规则)和用户体验优化(可视化选座)。我2016年参与过某连锁影院的系统升级,当时他们日均订单峰值达到2万+,这个量级足够考验系统的健壮性。
选择Java Web技术栈的优势很明显:Spring生态的成熟度能帮你快速搭建MVC架构,MyBatis的灵活性适合处理影院特有的多表关联查询(影片-影厅-场次-座位四级联查),而Servlet的线程模型天然适合处理瞬时购票请求。不过要注意,学校实验室环境和企业生产环境的最大区别在于:前者往往忽略事务隔离级别和锁竞争的问题。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 系统架构设计要点
2.1 技术选型建议
基础框架组合我推荐:
- 前端:Thymeleaf + Bootstrap 5(比JSP更现代)
- 后端:Spring Boot 2.7 + Spring MVC
- 持久层:MyBatis-Plus 3.5(自带代码生成器)
- 数据库:MySQL 8.0(需配置事务隔离级别为REPEATABLE_READ)
关键依赖示例(pom.xml片段):
xml复制<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>3.5.3</version>
</dependency>
2.2 核心业务表设计
四个必须精心设计的表:
- 影片表(film):注意字段
show_status要用ENUM('即将上映','热映中','已下架') - 影厅表(hall):重点在
seat_map字段存储JSON格式的座位矩阵 - 场次表(schedule):需要建立film_id和hall_id的外键,并添加
lock_time字段 - 订单表(order):必须包含
order_status和version(用于乐观锁)
建表示例:
sql复制CREATE TABLE `schedule` (
`id` bigint NOT NULL AUTO_INCREMENT,
`film_id` bigint NOT NULL COMMENT '冗余影片名称避免连表查询',
`hall_id` int NOT NULL,
`show_time` datetime NOT NULL,
`price` decimal(10,2) DEFAULT NULL,
`lock_time` int DEFAULT '30' COMMENT '座位锁定时长(分钟)',
PRIMARY KEY (`id`),
KEY `idx_film` (`film_id`),
KEY `idx_time` (`show_time`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
3. 高并发选座实现方案
3.1 座位锁定逻辑
核心难点在于防止超卖,我推荐两种方案:
- 悲观锁方案(适合初学者):
java复制@Transactional(isolation = Isolation.REPEATABLE_READ)
public boolean lockSeats(Long scheduleId, List<Integer> seatNumbers) {
// 先查询当前座位状态
Schedule schedule = scheduleMapper.selectByIdForUpdate(scheduleId); // FOR UPDATE锁定
// 校验座位是否可用
if(seatNumbers.stream().anyMatch(num ->
seatMapper.checkLocked(scheduleId, num) > 0)){
throw new BusinessException("座位已被锁定");
}
// 执行锁定
seatNumbers.forEach(num -> {
seatMapper.lockSeat(scheduleId, num,
LocalDateTime.now().plusMinutes(schedule.getLockTime()));
});
return true;
}
- 乐观锁+Redis方案(生产环境推荐):
java复制public boolean lockSeatsWithRedis(Long scheduleId, List<Integer> seatNumbers) {
String lockKey = "schedule:" + scheduleId;
// 使用Redis的SETNX实现分布式锁
Boolean locked = redisTemplate.opsForValue()
.setIfAbsent(lockKey, "1", 30, TimeUnit.SECONDS);
if(!locked) throw new BusinessException("系统繁忙请重试");
try {
// 使用Lua脚本保证原子性
String script = "local seats = redis.call('HGET', KEYS[1], 'seats')...";
redisTemplate.execute(new DefaultRedisScript<>(script, Boolean.class),
Collections.singletonList("schedule:" + scheduleId),
seatNumbers.toArray());
} finally {
redisTemplate.delete(lockKey);
}
}
3.2 超时订单处理
必须配置定时任务清理未支付订单:
java复制@Scheduled(cron = "0 */5 * * * ?")
public void releaseTimeoutSeats() {
List<Order> timeoutOrders = orderMapper.selectTimeoutOrders(
LocalDateTime.now().minusMinutes(15));
timeoutOrders.forEach(order -> {
seatMapper.batchUnlockSeats(order.getScheduleId(),
order.getSeatNumbers().split(","));
orderMapper.updateOrderStatus(order.getId(), OrderStatus.TIMEOUT);
});
}
4. 典型业务场景实现
4.1 选座页面性能优化
前端实现建议:
- 使用Canvas绘制座位图(参考算法):
javascript复制function drawSeatMap(rows, cols) {
const canvas = document.getElementById('seatMap');
const ctx = canvas.getContext('2d');
// 计算座位间距和大小
const seatSize = Math.min(canvas.width/cols*0.8, canvas.height/rows*0.8);
// 绘制座位
for(let row=0; row<rows; row++) {
for(let col=0; col<cols; col++) {
const x = col * (seatSize + 5) + 20;
const y = row * (seatSize + 5) + 20;
// 根据座位状态设置颜色
ctx.fillStyle = getSeatColor(row, col);
ctx.fillRect(x, y, seatSize, seatSize);
}
}
}
- 后端采用分级加载策略:
- 首屏只返回影厅基础结构
- 座位状态通过WebSocket实时更新
- 历史订单数据单独接口查询
4.2 支付流程设计
支付状态机设计要点:
mermaid复制stateDiagram-v2
[*] --> PENDING
PENDING --> PAID: 支付成功
PENDING --> TIMEOUT: 15分钟未支付
PENDING --> CANCELLED: 用户取消
PAID --> REFUNDED: 申请退款
对应状态转换代码:
java复制public enum OrderStatus {
PENDING(0, "待支付"),
PAID(1, "已支付"),
TIMEOUT(2, "已超时"),
CANCELLED(3, "已取消"),
REFUNDED(4, "已退款");
// 必须实现状态校验方法
public static boolean canTransfer(OrderStatus from, OrderStatus to) {
switch(from) {
case PENDING:
return to == PAID || to == TIMEOUT || to == CANCELLED;
case PAID:
return to == REFUNDED;
default:
return false;
}
}
}
5. 调试与部署实战
5.1 常见问题排查
- 座位锁定失效问题:
- 检查MySQL事务隔离级别:
SELECT @@transaction_isolation - 确认@Transactional注解生效
- 测试并发场景:使用JMeter模拟100并发请求
- 性能优化记录:
java复制// 在application.properties中添加:
spring.datasource.hikari.maximum-pool-size=20
spring.jpa.properties.hibernate.jdbc.batch_size=50
spring.jpa.properties.hibernate.order_inserts=true
5.2 部署注意事项
- Nginx配置要点:
nginx复制location / {
proxy_pass http://localhost:8080;
proxy_set_header X-Real-IP $remote_addr;
# 解决WebSocket代理问题
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
}
- 启动参数建议:
bash复制java -jar cinema.jar \
--spring.profiles.active=prod \
--server.tomcat.max-threads=200 \
--spring.redis.timeout=3000
6. 毕设答辩加分项
- 安全防护实现:
- 防SQL注入:使用MyBatis的#{}语法
- XSS防护:添加Spring Security的HttpSecurity配置
- CSRF防护:启用Thymeleaf的自动CSRF令牌
- 扩展功能建议:
- 会员积分系统(策略模式实现)
- 影片推荐算法(基于协同过滤)
- 票房统计报表(ECharts可视化)
- 性能测试报告模板:
| 测试场景 | 并发数 | 平均响应时间 | 错误率 |
|---------|--------|--------------|--------|
| 选座查询 | 100 | 238ms | 0% |
| 下单支付 | 50 | 1.2s | 2% |
这个项目最考验的是对并发问题的处理能力,建议在答辩时重点演示:
- 两个浏览器同时选相同座位的冲突处理
- 支付倒计时结束后的座位释放
- 影院管理员修改场次时的数据一致性保证
