1. 项目概述:影院订票系统的全栈架构解析
这套基于SpringBoot+Vue+MyBatis的企业级影院管理系统,是我参与过最典型的全栈项目实践。系统采用前后端分离架构,后端用SpringBoot提供RESTful API,前端用Vue构建响应式管理界面,MyBatis作为ORM层操作MySQL数据库。不同于简单的CRUD系统,它完整覆盖了影院运营的三大核心场景:会员购票、场次管理和票房统计。
在具体实现上,系统需要处理高并发座位锁定、电子票务核销、动态票价策略等业务难点。我曾用这套架构为本地连锁影院部署过线上系统,日均处理3000+订单,峰值QPS达到200+。下面就从技术选型到关键模块,拆解这个项目的实现要点。
2. 技术栈深度解析
2.1 SpringBoot后端设计要点
后端采用经典的MVC分层:
code复制com.cinema
├── config # 安全/缓存配置
├── controller # REST接口
├── service # 业务逻辑
├── dao # MyBatis映射
└── entity # 数据实体
关键配置示例(application.yml):
yaml复制spring:
datasource:
url: jdbc:mysql://localhost:3306/cinema?useSSL=false&serverTimezone=UTC
username: cinema_admin
password: 加密密码
redis: # 用于座位锁定
host: 127.0.0.1
port: 6379
特别注意:数据库连接必须配置时区(serverTimezone),否则Java日期类型与MySQL会出现8小时偏差
2.2 Vue前端工程化实践
前端采用Vue CLI搭建的SPA项目,核心目录结构:
code复制src/
├── api/ # Axios封装
├── assets/
├── components/ # 复用组件
│ ├── SeatSelector.vue # 座位选择器
│ └── DatePicker.vue
├── router/ # 路由守卫
└── views/ # 页面视图
性能优化点:
- 路由懒加载:
const User = () => import('./views/User.vue') - 接口请求拦截:在axios拦截器中统一添加JWT token
- 使用Vuex管理影院、影片等全局状态
2.3 MyBatis动态SQL技巧
在排片查询等复杂场景,使用MyBatis的动态SQL提升灵活性:
xml复制<select id="selectSchedules" resultType="ScheduleVO">
SELECT * FROM schedule
<where>
<if test="cinemaId != null">
AND cinema_id = #{cinemaId}
</if>
<if test="movieId != null">
AND movie_id = #{movieId}
</if>
<if test="date != null">
AND DATE(start_time) = #{date}
</if>
</where>
ORDER BY start_time
</select>
3. 核心业务模块实现
3.1 座位锁定与并发控制
采用Redis分布式锁防止超卖:
java复制public boolean lockSeats(List<Integer> seatIds, Long scheduleId) {
String lockKey = "lock:" + scheduleId;
// 获取Redis锁(SETNX实现)
Boolean locked = redisTemplate.opsForValue()
.setIfAbsent(lockKey, "1", 300, TimeUnit.SECONDS);
if(locked) {
try {
// 检查座位是否可用
List<Seat> seats = seatDao.selectByIds(seatIds);
if(seats.stream().anyMatch(s -> !s.isAvailable())) {
return false;
}
// 执行锁定(数据库+缓存)
seatDao.updateStatus(seatIds, SeatStatus.LOCKED);
redisTemplate.opsForSet().add("locked_seats:"+scheduleId, seatIds);
return true;
} finally {
redisTemplate.delete(lockKey);
}
}
return false;
}
踩坑记录:早期版本未设置锁过期时间,导致系统异常时座位永久锁定。务必添加合理的TTL!
3.2 电子票务生成方案
采用QR码+数字签名的防伪方案:
- 票务信息JSON结构:
json复制{
"orderNo": "20230801123456",
"scheduleId": 1024,
"seatNumbers": ["A1","A2"],
"timestamp": 1690848000,
"sign": "a1b2c3...(HMAC-SHA256签名)"
}
- 前端调用qrcode.js生成二维码
- 验票时后端校验签名有效性
3.3 动态票价策略实现
通过策略模式实现多种计价方式:
java复制public interface PricingStrategy {
BigDecimal calculatePrice(Schedule schedule, Seat seat);
}
@Component
@Qualifier("weekendPricing")
public class WeekendPricing implements PricingStrategy {
@Override
public BigDecimal calculatePrice(Schedule schedule, Seat seat) {
LocalDateTime time = schedule.getStartTime();
// 周末溢价20%
return time.getDayOfWeek().getValue() >= 6
? seat.getBasePrice().multiply(new BigDecimal("1.2"))
: seat.getBasePrice();
}
}
4. 数据库设计与优化
4.1 核心表结构
sql复制CREATE TABLE `schedule` (
`id` bigint NOT NULL AUTO_INCREMENT,
`cinema_id` bigint NOT NULL COMMENT '影院ID',
`movie_id` bigint NOT NULL COMMENT '影片ID',
`hall_id` int NOT NULL COMMENT '影厅ID',
`start_time` datetime NOT NULL COMMENT '开场时间',
`end_time` datetime NOT NULL COMMENT '结束时间',
`price` decimal(10,2) DEFAULT '0.00' COMMENT '基础票价',
PRIMARY KEY (`id`),
KEY `idx_cinema_movie` (`cinema_id`,`movie_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
4.2 查询优化案例
影厅座位状态查询优化前:
sql复制SELECT * FROM seat WHERE hall_id = 5; -- 全表扫描
优化后:
sql复制ALTER TABLE seat ADD INDEX idx_hall (hall_id);
EXPLAIN SELECT * FROM seat WHERE hall_id = 5; -- 使用索引
5. 部署与运维要点
5.1 生产环境配置建议
Nginx关键配置(部分):
nginx复制# 前端静态资源
server {
listen 80;
server_name cinema.example.com;
location / {
root /var/www/cinema/dist;
try_files $uri $uri/ /index.html;
}
}
# 后端API代理
server {
listen 80;
server_name api.cinema.example.com;
location / {
proxy_pass http://127.0.0.1:8080;
proxy_set_header Host $host;
}
}
5.2 监控指标设置
Prometheus监控指标示例:
yaml复制- job_name: 'cinema_backend'
metrics_path: '/actuator/prometheus'
static_configs:
- targets: ['192.168.1.10:8080']
relabel_configs:
- source_labels: [__address__]
target_label: instance
regex: '(.*):\d+'
replacement: '$1'
6. 典型问题排查实录
6.1 座位锁定失效问题
现象:高峰时段出现座位重复售卖
排查过程:
- 检查Redis锁日志,发现大量
SETNX失败 - 监控显示Redis连接数达到上限(默认100)
- 连接未及时释放导致锁获取失败
解决方案:
java复制// 修改RedisTemplate配置
@Bean
public RedisConnectionFactory redisConnectionFactory() {
LettuceConnectionFactory factory = new LettuceConnectionFactory();
factory.setPoolConfig(new GenericObjectPoolConfig() {{
setMaxTotal(200); // 扩大连接池
setMaxWaitMillis(1000);
}});
return factory;
}
6.2 订单超时处理方案
采用Spring定时任务+状态机:
java复制@Scheduled(fixedRate = 60000) // 每分钟执行
public void cancelExpiredOrders() {
List<Order> orders = orderDao.selectExpiredOrders();
orders.forEach(order -> {
order.setStatus(OrderStatus.CANCELLED);
orderDao.update(order);
// 释放座位
seatService.unlockSeats(order.getSeatIds());
});
}
7. 扩展功能建议
7.1 三方服务集成
- 短信通知(阿里云短信API):
java复制public void sendSms(String phone, String templateCode, Map<String,String> params) {
DefaultProfile profile = DefaultProfile.getProfile(
"cn-hangzhou", accessKeyId, accessKeySecret);
IAcsClient client = new DefaultAcsClient(profile);
CommonRequest request = new CommonRequest();
request.setSysDomain("dysmsapi.aliyuncs.com");
request.setSysVersion("2017-05-25");
request.setSysAction("SendSms");
request.putQueryParameter("PhoneNumbers", phone);
request.putQueryParameter("TemplateCode", templateCode);
request.putQueryParameter("TemplateParam", JSON.toJSONString(params));
client.getCommonResponse(request);
}
7.2 数据分析看板
基于Elasticsearch的票房统计:
json复制// 索引映射
PUT /ticket_sales
{
"mappings": {
"properties": {
"scheduleId": { "type": "keyword" },
"movieName": { "type": "text" },
"cinemaName": { "type": "keyword" },
"salesAmount": { "type": "double" },
"playTime": { "type": "date" }
}
}
}
这套系统在实际运营中,建议根据业务规模逐步引入:
- 初期:直接使用源码部署
- 成长期:增加Redis集群、读写分离
- 大型连锁:考虑微服务化拆分(订单服务、排片服务等)