作为一名有多年Java开发经验的工程师,最近完成了一个基于Spring Boot的电影售票系统。这个系统采用了前后端分离的架构,前端使用HTML/CSS/JavaScript,后端基于Spring Boot框架,数据库选用MySQL。系统主要面向三类用户:普通观众、影院员工和系统管理员,实现了从电影信息展示到票务管理的全流程功能。
在实际开发过程中,我发现很多初学者在构建类似系统时容易陷入一些常见陷阱。比如数据库设计不合理导致查询性能低下,或者权限控制不严谨引发安全问题。本文将分享我在开发这个系统时的完整思路和关键实现细节,希望能为需要开发类似系统的同学提供参考。
选择Spring Boot作为后端框架主要基于以下几个考虑:
数据库选择MySQL 8.0版本,主要因为:
前端采用传统三件套(HTML/CSS/JS)而非现代框架,主要考虑:
系统采用经典的三层架构:
code复制表示层(Web)
↓
业务逻辑层(Service)
↓
数据访问层(Repository)
每层的职责明确:
这种分层带来的好处是:
系统采用基于角色的访问控制(RBAC)模型:
java复制@Entity
public class User {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String username;
private String password;
@ManyToOne
@JoinColumn(name = "role_id")
private Role role;
// getters/setters...
}
@Entity
public class Role {
@Id
@GeneratedValue
private Long id;
private String name;
@ManyToMany
private Set<Permission> permissions;
// getters/setters...
}
关键实现点:
注意:生产环境一定要启用HTTPS,防止token被截获
票务是系统的核心功能,主要涉及以下几个实体:
java复制@Entity
public class Movie {
private String title;
private String director;
private Integer duration; //分钟
// 其他基本信息...
}
@Entity
public class Screening {
@ManyToOne
private Movie movie;
private LocalDateTime startTime;
@ManyToOne
private Theater theater;
private BigDecimal price;
// 场次信息...
}
@Entity
public class Booking {
@ManyToOne
private User user;
@ManyToOne
private Screening screening;
private String seatNumber;
private BookingStatus status;
// 预订信息...
}
购票流程的关键代码:
java复制@Transactional
public BookingResult bookTicket(Long userId, Long screeningId, String seat) {
// 1. 检查座位是否可用
if(bookingRepository.existsByScreeningIdAndSeat(screeningId, seat)) {
throw new SeatAlreadyTakenException();
}
// 2. 创建预订记录
Booking booking = new Booking();
booking.setUser(userRepository.findById(userId).orElseThrow());
booking.setScreening(screeningRepository.findById(screeningId).orElseThrow());
booking.setSeat(seat);
booking.setStatus(BookingStatus.PENDING_PAYMENT);
// 3. 预留座位(15分钟支付超时)
bookingRepository.save(booking);
schedulePaymentTimeout(booking.getId());
return new BookingResult(booking.getId());
}
系统接入了支付宝和微信支付SDK,关键实现:
java复制public interface PaymentService {
PaymentResponse createPayment(PaymentRequest request);
PaymentStatus checkPayment(String paymentId);
}
@Service
@RequiredArgsConstructor
public class AlipayService implements PaymentService {
private final AlipayClient alipayClient;
public PaymentResponse createPayment(PaymentRequest request) {
AlipayTradePagePayRequest alipayRequest = new AlipayTradePagePayRequest();
alipayRequest.setBizContent(buildBizContent(request));
return alipayClient.pageExecute(alipayRequest);
}
// 其他实现...
}
支付流程注意事项:
主要表关系如下:
code复制用户表(user) ← 预订表(booking) → 场次表(screening) → 电影表(movie)
↓
影厅表(theater)
索引设计示例:
sql复制-- 加快场次查询
CREATE INDEX idx_screening_movie ON screening(movie_id);
CREATE INDEX idx_screening_time ON screening(start_time);
-- 加速座位检查
CREATE UNIQUE INDEX idx_booking_seat ON booking(screening_id, seat_number)
WHERE status != 'CANCELLED';
java复制// 不好的写法 - N+1查询问题
List<Screening> screenings = screeningRepository.findAll();
screenings.forEach(s -> s.getMovie().getTitle()); // 每次getTitle都会触发查询
// 好的写法 - 一次性加载关联数据
@Query("SELECT s FROM Screening s JOIN FETCH s.movie WHERE s.startTime > :now")
List<Screening> findUpcomingScreenings(@Param("now") LocalDateTime now);
java复制@Transactional
public boolean lockSeat(Long screeningId, String seat) {
return jdbcTemplate.update(
"INSERT INTO seat_lock(screening_id, seat, expires_at) " +
"VALUES (?, ?, ?) ON CONFLICT DO NOTHING",
screeningId, seat, LocalDateTime.now().plusMinutes(15)
) > 0;
}
java复制@Transactional(isolation = Isolation.SERIALIZABLE)
public Booking createBooking(...) {
int available = getAvailableSeats(screeningId);
if(available <= 0) {
throw new NoSeatAvailableException();
}
// 创建订单...
}
推荐使用Docker Compose部署:
yaml复制version: '3'
services:
db:
image: mysql:8.0
environment:
MYSQL_ROOT_PASSWORD: ${DB_PASSWORD}
volumes:
- mysql_data:/var/lib/mysql
app:
build: .
environment:
SPRING_DATASOURCE_URL: jdbc:mysql://db:3306/cinema
ports:
- "8080:8080"
depends_on:
- db
volumes:
mysql_data:
核心监控项:
使用Prometheus + Grafana搭建监控看板。
在开发过程中,我积累了一些有价值的经验:
并发控制:票务系统必须处理好并发问题,特别是在热门影片开售时。除了数据库事务,还可以考虑:
缓存策略:
异常处理:
java复制// 不好的做法
try {
paymentService.process(order);
} catch (Exception e) {
e.printStackTrace();
}
// 好的做法
try {
paymentService.process(order);
} catch (PaymentException e) {
log.error("Payment failed for order {}", order.getId(), e);
notifyUser(order.getUser(), "支付失败,请重试");
releaseSeats(order);
}
测试建议:
这个项目让我深刻体会到,一个好的票务系统不仅要有完善的功能,更需要考虑性能、安全性和用户体验。特别是在高并发场景下,如何平衡数据一致性和系统吞吐量是需要精心设计的。