1. 项目背景与核心功能解析
这个基于Java SSM框架的电影购票选座推荐系统,是我在疫情期间为本地影院开发的一个实战项目。当时线下影院面临生存危机,急需一套能提升线上购票体验的解决方案。系统最核心的创新点在于将传统选座功能与智能推荐算法结合,解决了"选座纠结症"这个普遍痛点。
系统主要包含三大功能模块:
- 影院场次管理:采用树形结构存储放映厅座位数据,每个座位用二维坐标(x,y)标记位置,配合Redis缓存热门场次
- 动态选座推荐:基于用户历史选座偏好(如偏好中间区域)和实时座位销售情况,使用协同过滤算法生成推荐座位集
- 分布式锁票机制:采用Redisson实现分布式锁,防止高并发下的座位超卖问题
关键设计决策:选用SSM框架而非Spring Boot,主要是考虑到影院原有系统都是基于XML配置,需要保持技术栈统一。MyBatis的二级缓存配置对高频查询的场次数据性能提升显著。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 技术架构深度拆解
2.1 分层架构设计
系统采用经典三层架构:
code复制表现层:JSP+JSTL+EL
业务层:Spring 5.3.18(声明式事务管理)
持久层:MyBatis 3.5.6(定制化TypeHandler处理座位状态)
2.2 数据库关键表设计
sql复制CREATE TABLE `t_seat` (
`seat_id` varchar(32) NOT NULL COMMENT '采用行号+列号组合ID',
`screen_id` int(11) NOT NULL,
`x_coordinate` int(11) DEFAULT NULL COMMENT '横坐标',
`y_coordinate` int(11) DEFAULT NULL COMMENT '纵坐标',
`seat_type` tinyint(4) DEFAULT '1' COMMENT '1普通座 2情侣座 3残疾座',
PRIMARY KEY (`seat_id`),
KEY `idx_screen` (`screen_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4
2.3 推荐算法实现
选座推荐核心逻辑:
java复制public List<Seat> recommendSeats(User user, Screening screening) {
// 1. 获取用户历史偏好
Preference preference = preferenceDao.selectByUser(user.getId());
// 2. 获取当前场次已售座位
Set<String> soldSeats = ticketService.getSoldSeats(screening.getId());
// 3. 生成推荐候选集
return seatMatrix.stream()
.filter(seat -> !soldSeats.contains(seat.getId()))
.sorted(comparing(seat ->
calculateScore(seat, preference))) // 综合距离、视角等因素
.limit(5)
.collect(Collectors.toList());
}
3. 开发环境搭建指南
3.1 IDEA项目配置要点
-
必须配置的插件:
- Lombok(处理实体类)
- MyBatisX(XML跳转)
- JRebel(热部署)
-
容易踩坑的配置:
xml复制<!-- pom.xml关键依赖 -->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.18.24</version>
<scope>provided</scope> <!-- 必须设为provided -->
</dependency>
3.2 数据库连接池优化
properties复制# application.properties
spring.datasource.druid.initial-size=5
spring.datasource.druid.max-active=20
spring.datasource.druid.validation-query=SELECT 1 FROM DUAL
spring.datasource.druid.test-while-idle=true
4. 典型业务场景实现
4.1 选座并发控制
采用Redisson分布式锁实现:
java复制public boolean lockSeats(List<String> seatIds) {
RLock lock = redissonClient.getLock("LOCK_SEATS:" + String.join(",", seatIds));
try {
if (lock.tryLock(3, 10, TimeUnit.SECONDS)) {
// 检查座位状态
if (seatDao.checkAvailable(seatIds)) {
seatDao.updateStatus(seatIds, SeatStatus.LOCKED);
return true;
}
}
} finally {
lock.unlock();
}
return false;
}
4.2 支付超时处理
使用Spring定时任务释放过期座位:
java复制@Scheduled(cron = "0 */5 * * * ?")
public void releaseExpiredSeats() {
List<Seat> expired = seatDao.selectExpiredLocks(15); // 15分钟未支付
expired.forEach(seat -> {
seat.setStatus(SeatStatus.AVAILABLE);
seatDao.update(seat);
redisTemplate.opsForValue().getAndDelete("LOCK:" + seat.getId());
});
}
5. 性能优化实战技巧
5.1 MyBatis二级缓存配置
xml复制<mapper namespace="com.cinema.dao.SeatDao">
<cache eviction="LRU"
flushInterval="60000"
size="1024"
readOnly="true"/>
<select id="selectByScreen" resultType="Seat" useCache="true">
SELECT * FROM t_seat WHERE screen_id = #{screenId}
</select>
</mapper>
5.2 前端座位渲染优化
采用Canvas绘制座位图替代DOM操作:
javascript复制function renderSeats(matrix) {
const canvas = document.getElementById('seat-map');
const ctx = canvas.getContext('2d');
matrix.forEach(row => {
row.forEach(seat => {
ctx.fillStyle = getSeatColor(seat.status);
ctx.fillRect(seat.x * 30, seat.y * 30, 25, 25);
});
});
}
6. 部署上线注意事项
- Nginx静态资源配置:
nginx复制location /static {
alias /opt/cinema/static;
expires 30d;
add_header Cache-Control "public";
}
location / {
proxy_pass http://127.0.0.1:8080;
proxy_set_header X-Real-IP $remote_addr;
}
- JVM参数建议:
bash复制java -jar -Xms512m -Xmx1024m -XX:MetaspaceSize=128m \
-XX:MaxMetaspaceSize=256m -Dfile.encoding=UTF-8 \
cinema.jar
7. 项目扩展方向
- 推荐算法升级:引入机器学习模型分析用户行为数据
- 可视化监控:集成Prometheus监控关键指标
- 小程序端:基于uni-app开发跨平台客户端
在项目开发过程中,最耗时的部分是处理座位状态同步问题。我们最终采用的解决方案是:Redis Pub/Sub + 数据库乐观锁的双重保障机制。当某个座位状态变更时,通过Redis频道广播事件,各节点接收到通知后刷新本地缓存。
