作为一名有多年开发经验的Java工程师,最近完成了一个基于BS架构的影院售票系统。这个系统采用Spring Boot+Vue的前后端分离架构,实现了从影片管理、场次安排到在线购票、订单管理的全流程数字化解决方案。相比传统售票方式,这套系统将购票响应时间从平均3分钟缩短到15秒内,同时减少了80%的人工操作错误。
在技术选型上,我主要考虑了以下几个关键因素:
最终确定的技术栈如下表所示:
| 层级 | 技术选型 | 版本 | 选择理由 |
|---|---|---|---|
| 前端 | Vue.js | 2.6.x | 渐进式框架,组件化开发 |
| 前端UI | Element UI | 2.15.x | 丰富的组件库,开发效率高 |
| 后端 | Spring Boot | 2.5.x | 快速开发,微服务友好 |
| ORM | MyBatis Plus | 3.4.x | 简化CRUD操作 |
| 数据库 | MySQL | 8.0 | 事务支持完善,性能稳定 |
| 缓存 | Redis | 6.x | 应对高并发场景 |
系统采用经典的三层架构:
code复制表示层(Vue) → 业务逻辑层(Spring Boot) → 数据访问层(MyBatis Plus)
↓
Redis缓存
这种架构的优势在于:
关键代码实现:
java复制// JWT生成
public String generateToken(UserDetails userDetails) {
Map<String, Object> claims = new HashMap<>();
return Jwts.builder()
.setClaims(claims)
.setSubject(userDetails.getUsername())
.setIssuedAt(new Date(System.currentTimeMillis()))
.setExpiration(new Date(System.currentTimeMillis() + JWT_TOKEN_VALIDITY * 1000))
.signWith(SignatureAlgorithm.HS512, secret)
.compact();
}
// 过滤器验证
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain)
throws ServletException, IOException {
String token = getJwtFromRequest(request);
if (StringUtils.hasText(token) && jwtUtil.validateToken(token)) {
String username = jwtUtil.getUsernameFromToken(token);
UserDetails userDetails = userDetailsService.loadUserByUsername(username);
UsernamePasswordAuthenticationToken authentication =
new UsernamePasswordAuthenticationToken(userDetails, null, userDetails.getAuthorities());
authentication.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
SecurityContextHolder.getContext().setAuthentication(authentication);
}
chain.doFilter(request, response);
}
影片信息表(movie_info)关键字段:
| 字段名 | 类型 | 说明 |
|---|---|---|
| id | bigint | 主键 |
| title | varchar(100) | 影片名称 |
| director | varchar(50) | 导演 |
| actors | text | 主演列表 |
| duration | int | 时长(分钟) |
| release_date | date | 上映日期 |
| price | decimal(10,2) | 基础票价 |
| status | tinyint | 0-未上映 1-热映中 2-已下架 |
使用Redis二级缓存热门影片信息:
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);
}
缓存配置要点:
为解决高并发下的座位冲突问题,设计了三级锁定策略:
关键实现代码:
java复制public boolean lockSeats(List<Long> seatIds, Long userId) {
String lockKey = "seat_lock:" + StringUtils.join(seatIds, ",");
// 尝试获取分布式锁
Boolean locked = redisTemplate.opsForValue().setIfAbsent(lockKey, userId.toString(), 5, TimeUnit.MINUTES);
if (locked != null && locked) {
try {
// 检查座位状态
List<Seat> seats = seatMapper.selectBatchIds(seatIds);
if (seats.stream().anyMatch(s -> s.getStatus() != SeatStatus.AVAILABLE)) {
return false;
}
// 更新座位状态
seatMapper.lockSeats(seatIds, userId);
return true;
} finally {
redisTemplate.delete(lockKey);
}
}
return false;
}
支付状态机设计:
code复制待支付 → 支付中 → 支付成功
↓ ↓
└→ 支付失败 ←┘
支付超时处理:
java复制@Scheduled(fixedRate = 60000)
public void checkPaymentTimeout() {
List<Order> timeoutOrders = orderMapper.selectTimeoutOrders(PAYMENT_TIMEOUT_MINUTES);
timeoutOrders.forEach(order -> {
order.setStatus(OrderStatus.CANCELLED);
orderMapper.updateById(order);
// 释放座位
seatService.releaseSeats(order.getSeatIds());
});
}
索引优化:
ALTER TABLE schedule ADD INDEX idx_movie_time (movie_id, show_time)SQL优化:
javascript复制const MovieList = () => import('./views/MovieList.vue')
javascript复制// 使用axios的拦截器合并短时间内相同请求
let pendingRequests = new Map()
axios.interceptors.request.use(config => {
const requestKey = `${config.method}-${config.url}-${JSON.stringify(config.params)}`
if (pendingRequests.has(requestKey)) {
return pendingRequests.get(requestKey)
}
const request = axios(config)
pendingRequests.set(requestKey, request)
request.finally(() => pendingRequests.delete(requestKey))
return request
})
使用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"
分布式锁问题:
事务失效场景:
Vue响应式问题:
日期处理陷阱:
这个项目从需求分析到最终上线历时3个月,期间遇到了不少技术挑战,但也积累了宝贵的实战经验。特别是在高并发场景下的座位锁定机制,经过多次优化后,在压力测试中可以达到500+ TPS的性能表现。