1. 项目背景与需求分析
作为一名经历过多个Web项目开发的工程师,我深知电影院购票系统这类业务场景的技术实现难点。传统的线下购票方式确实存在诸多痛点:周末排队时间长、热门场次售罄信息获取不及时、座位选择不直观等。这些问题不仅影响用户体验,也制约了影院的运营效率。
基于Spring Boot和Vue.js的全栈解决方案,能够很好地解决这些痛点。Spring Boot的后端稳定性与Vue.js的前端交互性相结合,可以打造一个响应迅速、操作直观的购票平台。我在实际开发中发现,这种技术组合特别适合需要快速迭代的中小型项目。
2. 系统架构设计
2.1 技术选型考量
选择Spring Boot作为后端框架主要基于以下考虑:
- 内嵌Tomcat服务器,简化部署流程
- 自动配置特性大幅减少XML配置
- 丰富的Starter依赖,快速集成MyBatis等组件
- 完善的监控机制(Actuator)
前端选用Vue.js的原因:
- 组件化开发模式适合购票系统的模块化需求
- 响应式数据绑定简化座位选择等交互逻辑
- Vue Router实现流畅的单页应用体验
- Element UI提供现成的美观组件
2.2 系统分层架构
code复制┌───────────────────────────────────────┐
│ 客户端层 │
│ ┌───────────┐ ┌───────────┐ │
│ │ Web │ │ Mobile │ │
│ │ (Vue.js) │ │ (可选) │ │
│ └───────────┘ └───────────┘ │
└───────────────┬───────────────────────┘
│ HTTP/HTTPS
┌───────────────▼───────────────────────┐
│ API网关层 │
│ ┌───────────────────────────────┐ │
│ │ Spring Cloud Gateway │ │
│ └───────────────────────────────┘ │
└───────────────┬───────────────────────┘
│
┌───────────────▼───────────────────────┐
│ 业务服务层 │
│ ┌───────────┐ ┌───────────┐ │
│ │ 用户服务 │ │ 订单服务 │ │
│ └───────────┘ └───────────┘ │
│ ┌───────────┐ ┌───────────┐ │
│ │ 影片服务 │ │ 支付服务 │ │
│ └───────────┘ └───────────┘ │
└───────────────┬───────────────────────┘
│ JDBC
┌───────────────▼───────────────────────┐
│ 数据持久层 │
│ ┌───────────────────────────────┐ │
│ │ MySQL 8.0 │ │
│ └───────────────────────────────┘ │
└───────────────────────────────────────┘
3. 核心功能实现细节
3.1 用户模块设计
用户表结构设计示例:
sql复制CREATE TABLE `user` (
`id` bigint(20) NOT NULL AUTO_INCREMENT,
`username` varchar(50) NOT NULL COMMENT '登录账号',
`password` varchar(100) NOT NULL COMMENT '加密后的密码',
`salt` varchar(20) DEFAULT NULL COMMENT '加密盐值',
`phone` varchar(20) DEFAULT NULL COMMENT '手机号',
`avatar` varchar(255) DEFAULT NULL COMMENT '头像URL',
`create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP,
`update_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
PRIMARY KEY (`id`),
UNIQUE KEY `idx_username` (`username`),
KEY `idx_phone` (`phone`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='用户表';
密码加密采用SHA-256加盐算法:
java复制public class PasswordUtil {
private static final String SHA256 = "SHA-256";
public static String encrypt(String password, String salt) {
try {
MessageDigest md = MessageDigest.getInstance(SHA256);
md.update((password + salt).getBytes());
return bytesToHex(md.digest());
} catch (NoSuchAlgorithmException e) {
throw new RuntimeException(e);
}
}
private static String bytesToHex(byte[] bytes) {
StringBuilder result = new StringBuilder();
for (byte b : bytes) {
result.append(String.format("%02x", b));
}
return result.toString();
}
}
3.2 电影场次与座位管理
场次表设计关键字段:
sql复制CREATE TABLE `schedule` (
`id` bigint(20) NOT NULL AUTO_INCREMENT,
`movie_id` bigint(20) NOT NULL COMMENT '电影ID',
`cinema_hall_id` bigint(20) NOT NULL COMMENT '影厅ID',
`start_time` datetime NOT NULL COMMENT '开始时间',
`end_time` datetime NOT NULL COMMENT '结束时间',
`price` decimal(10,2) NOT NULL COMMENT '票价',
`status` tinyint(4) NOT NULL DEFAULT '1' COMMENT '状态:1可售 0停售',
PRIMARY KEY (`id`),
KEY `idx_movie` (`movie_id`),
KEY `idx_time` (`start_time`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
座位锁定逻辑实现(伪代码):
java复制@Transactional
public boolean lockSeats(Long scheduleId, List<Long> seatIds, Long userId) {
// 1. 检查座位是否可用
List<Seat> seats = seatMapper.selectByIds(seatIds);
if (seats.stream().anyMatch(s -> !s.isAvailable())) {
throw new BusinessException("部分座位已被预定");
}
// 2. 创建临时锁定记录(有效期15分钟)
String lockId = UUID.randomUUID().toString();
seatLockMapper.batchInsert(seatIds.stream()
.map(seatId -> new SeatLock(lockId, seatId, scheduleId, userId))
.collect(Collectors.toList()));
// 3. 更新座位状态
seatMapper.batchUpdateStatus(seatIds, SeatStatus.LOCKED);
return true;
}
4. 前端关键交互实现
4.1 座位选择组件
Vue组件核心代码结构:
vue复制<template>
<div class="seat-map">
<div class="screen">银幕</div>
<div class="seats-container">
<div
v-for="row in seatsLayout"
:key="row.row"
class="seat-row"
>
<div
v-for="seat in row.seats"
:key="seat.id"
:class="['seat', seat.status, { selected: selectedSeats.includes(seat.id) }]"
@click="toggleSeat(seat)"
>
{{ seat.number }}
</div>
</div>
</div>
</div>
</template>
<script>
export default {
props: {
scheduleId: { type: Number, required: true }
},
data() {
return {
seatsLayout: [],
selectedSeats: []
}
},
methods: {
async fetchSeats() {
const { data } = await axios.get(`/api/schedules/${this.scheduleId}/seats`);
this.seatsLayout = this.formatSeatLayout(data);
},
toggleSeat(seat) {
if (seat.status === 'available') {
const index = this.selectedSeats.indexOf(seat.id);
if (index === -1) {
this.selectedSeats.push(seat.id);
} else {
this.selectedSeats.splice(index, 1);
}
}
}
}
}
</script>
4.2 订单支付流程
支付状态机设计:
javascript复制const paymentStates = {
INIT: {
next: 'CREATED',
action: 'createOrder'
},
CREATED: {
next: 'PAYING',
action: 'requestPayment'
},
PAYING: {
next: ['PAID', 'FAILED'],
action: 'pollPayment'
},
PAID: {
next: 'COMPLETED',
action: 'confirmOrder'
},
FAILED: {
next: 'CLOSED',
action: 'cancelOrder'
}
};
class PaymentFlow {
constructor() {
this.currentState = 'INIT';
}
async transition(actionPayload) {
const stateConfig = paymentStates[this.currentState];
const result = await this[stateConfig.action](actionPayload);
if (Array.isArray(stateConfig.next)) {
this.currentState = result.success ? stateConfig.next[0] : stateConfig.next[1];
} else {
this.currentState = stateConfig.next;
}
return this.currentState;
}
}
5. 性能优化实践
5.1 缓存策略设计
采用多级缓存架构:
- 前端本地缓存:使用localStorage缓存电影列表等不常变数据
- 分布式Redis缓存:
- 热点数据:电影详情、场次信息
- 会话数据:用户购物车、临时订单
- 数据库查询缓存:MyBatis二级缓存
缓存更新策略示例:
java复制@Cacheable(value = "movies", key = "#id")
public Movie getMovieById(Long id) {
return movieMapper.selectById(id);
}
@CacheEvict(value = "movies", key = "#movie.id")
public void updateMovie(Movie movie) {
movieMapper.updateById(movie);
}
@Scheduled(fixedRate = 30 * 60 * 1000)
public void preloadHotMovies() {
List<Long> hotMovieIds = getHotMovieIdsFromDB();
hotMovieIds.forEach(id -> {
Movie movie = getMovieById(id);
cacheManager.getCache("movies").put(id, movie);
});
}
5.2 高并发处理方案
针对秒杀场次的解决方案:
- 库存预扣减:Redis原子操作
java复制public boolean reduceInventory(Long scheduleId, int count) {
String key = "inventory:" + scheduleId;
long value = redisTemplate.opsForValue().decrement(key, count);
if (value < 0) {
redisTemplate.opsForValue().increment(key, count);
return false;
}
return true;
}
- 消息队列削峰:
java复制@RabbitListener(queues = "order.create.queue")
public void processOrderCreate(OrderMessage message) {
try {
orderService.createOrder(message);
} catch (Exception e) {
log.error("订单处理失败", e);
// 加入重试队列
rabbitTemplate.convertAndSend(
"order.retry.exchange",
"order.retry.routing",
message
);
}
}
- 限流措施:
java复制@RestController
@RequestMapping("/api")
public class ApiController {
@RateLimiter(value = 100, key = "'movieList'")
@GetMapping("/movies")
public List<Movie> listMovies() {
return movieService.listAll();
}
}
6. 安全防护措施
6.1 常见Web安全防护
- XSS防护:
java复制@Configuration
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity http) throws Exception {
http.headers()
.xssProtection()
.and()
.contentSecurityPolicy("script-src 'self'");
}
}
- CSRF防护(Vue.js端):
javascript复制// axios拦截器设置
axios.interceptors.request.use(config => {
config.headers['X-Requested-With'] = 'XMLHttpRequest';
const token = getCookie('XSRF-TOKEN');
if (token) {
config.headers['X-XSRF-TOKEN'] = token;
}
return config;
});
- SQL注入防护:
- 严格使用MyBatis参数绑定
- 定期SQL审计
6.2 支付安全方案
- 敏感数据加密:
java复制public class PaymentService {
@Value("${aes.key}")
private String aesKey;
public String encryptCardInfo(CardInfo cardInfo) {
AES aes = new AES(aesKey.getBytes());
return aes.encrypt(JSON.toJSONString(cardInfo));
}
}
- 交易签名验证:
java复制public boolean verifySign(PaymentRequest request) {
String plainText = request.getOrderId() + request.getAmount();
String serverSign = HmacSHA256.sign(plainText, secretKey);
return serverSign.equals(request.getSign());
}
7. 部署与监控
7.1 容器化部署方案
Docker Compose配置示例:
yaml复制version: '3'
services:
mysql:
image: mysql:8.0
environment:
MYSQL_ROOT_PASSWORD: ${DB_PASSWORD}
volumes:
- mysql_data:/var/lib/mysql
ports:
- "3306:3306"
redis:
image: redis:6
ports:
- "6379:6379"
backend:
build: ./backend
ports:
- "8080:8080"
depends_on:
- mysql
- redis
frontend:
build: ./frontend
ports:
- "80:80"
volumes:
mysql_data:
7.2 监控体系搭建
Spring Boot Actuator配置:
yaml复制management:
endpoints:
web:
exposure:
include: health,info,metrics,prometheus
metrics:
export:
prometheus:
enabled: true
endpoint:
health:
show-details: always
Grafana监控面板配置:
- JVM监控:内存、线程、GC情况
- 业务指标:订单创建量、支付成功率
- 系统指标:CPU、内存、磁盘使用率
8. 开发经验与避坑指南
8.1 事务处理注意事项
- 避免大事务:
java复制// 反例 - 包含远程调用的大事务
@Transactional
public void createOrder(OrderDTO dto) {
// 1. 验证库存(远程调用)
inventoryService.check(dto.getItems()); // 问题点
// 2. 创建订单(本地事务)
orderMapper.insert(dto);
// 3. 扣减库存(本地事务)
inventoryMapper.reduce(dto.getItems());
}
改进方案:
java复制public void createOrder(OrderDTO dto) {
// 1. 预扣减库存(快速失败)
boolean success = inventoryService.tryReduce(dto.getItems());
if (!success) {
throw new BusinessException("库存不足");
}
// 2. 创建订单(独立事务)
transactionTemplate.execute(status -> {
orderMapper.insert(dto);
return true;
});
// 3. 异步确认库存
inventoryService.confirmReduce(dto.getItems());
}
8.2 前后端协作建议
- 接口规范:
- 统一响应格式:
typescript复制interface ApiResponse<T> {
code: number;
message: string;
data: T;
timestamp: number;
}
- 枚举值处理:
- 后端返回数字编码
- 前端维护枚举映射:
typescript复制enum OrderStatus {
UNPAID = 0,
PAID = 1,
COMPLETED = 2,
CANCELLED = 3
}
const statusText = {
[OrderStatus.UNPAID]: '待支付',
[OrderStatus.PAID]: '已支付',
// ...
};
- 联调技巧:
- 使用Mock.js开发阶段模拟接口
- 制定Swagger文档规范
- 约定错误码处理规范
9. 扩展功能展望
虽然基础功能已经完备,但在实际运营中还可以考虑以下扩展方向:
- 推荐系统集成:
- 基于用户历史的协同过滤推荐
- 基于电影特征的相似推荐
- 实时热门榜单
- 会员成长体系:
- 积分获取与兑换规则
- 等级特权设计
- 会员专属优惠
- 数据分析看板:
- 上座率分析
- 票房趋势预测
- 用户画像分析
- 多端适配:
- 微信小程序版本
- 自助取票机对接
- 影院员工APP
在实现这些扩展功能时,建议采用微服务架构进行渐进式重构,将核心功能与扩展功能解耦,通过领域驱动设计划分清晰的上下文边界。