1. 项目背景与需求分析
汽车票务系统作为传统客运行业的核心业务支撑,长期以来面临着线下运营效率低下、用户体验差等痛点。我在实际参与某省客运集团数字化转型项目时,深刻体会到传统售票窗口模式存在三大核心问题:
- 高峰期排队拥堵:节假日期间,车站售票窗口平均等待时间超过45分钟,用户满意度低于60%
- 座位资源浪费:由于信息不透明,线路空座率常年维持在30%左右
- 管理成本高企:人工统计报表误差率高达15%,财务对账需要3个工作日
基于SpringBoot+Vue的汽车票网上预订系统正是为解决这些问题而设计。通过实际项目验证,线上系统可将购票时间缩短至3分钟内,座位利用率提升至85%以上,财务对账实现实时化。这个方案特别适合中小型客运企业进行数字化转型时参考。
2. 技术架构设计
2.1 整体架构设计
系统采用经典的前后端分离架构,这是经过多个项目验证的高效协作模式:
code复制[浏览器] ←HTTP→ [Vue前端服务器] ←RESTful API→ [SpringBoot后端] ←JDBC→ [MySQL]
选择这种架构主要基于三点考虑:
- 开发效率:前后端可以并行开发,接口定义好后互不阻塞
- 性能优化:前端静态资源可单独部署CDN,减轻后端压力
- 技术栈专精:Vue擅长处理复杂交互,SpringBoot适合业务逻辑开发
2.2 关键技术选型
后端技术栈
- Spring Boot 2.7.x:相比传统SSM框架,省去了90%的XML配置
- MyBatis-Plus 3.5.x:内置通用CRUD操作,减少30%的重复SQL编写
- Hutool 5.8.x:工具类库处理日期、加密等常见操作
实际开发中发现MyBatis-Plus的Lambda查询特别实用,例如:
java复制// 查询明天出发的杭州到上海班次 lambdaQuery() .eq(Schedule::getDepartureCity, "杭州") .eq(Schedule::getArrivalCity, "上海") .ge(Schedule::getDepartureTime, DateUtil.tomorrow()) .lt(Schedule::getDepartureTime, DateUtil.tomorrow().offset(DateField.DAY_OF_YEAR, 1)) .list();
前端技术栈
- Vue 3.x:组合式API比Options API更灵活
- Element Plus:表单验证和表格组件极大提升开发效率
- Axios:配合拦截器实现全局401跳转登录页
3. 核心功能实现
3.1 车次余票查询
这是系统最核心的高并发场景,我们采用三级缓存策略:
- 本地缓存:使用Caffeine缓存最近1小时的热门线路
- Redis缓存:存储所有线路未来3天的余票数
- 数据库:最终一致性保障,通过定时任务每5分钟同步一次
关键实现代码:
java复制@Cacheable(value = "schedule", key = "#departure+'-'+#arrival")
public List<ScheduleVO> querySchedules(String departure, String arrival) {
// 先查Redis
String redisKey = "ticket:" + departure + ":" + arrival;
Object cache = redisTemplate.opsForValue().get(redisKey);
if (cache != null) {
return JSON.parseArray(cache.toString(), ScheduleVO.class);
}
// 查数据库
List<Schedule> list = scheduleMapper.selectList(...);
// 写入Redis并设置5分钟过期
redisTemplate.opsForValue().set(
redisKey,
JSON.toJSONString(list),
5, TimeUnit.MINUTES);
return convertToVO(list);
}
3.2 座位锁定机制
为防止超卖,采用分布式锁+数据库乐观锁方案:
- 用户选座时先获取Redis分布式锁(key=车次ID+座位号)
- 锁成功后执行数据库更新:
sql复制UPDATE schedule_seat
SET status = 2 -- 2表示锁定中
WHERE schedule_id = ? AND seat_no = ? AND status = 1 -- 1表示可售
- 15分钟未支付自动释放:
java复制@Scheduled(fixedRate = 60000) // 每分钟扫描
public void releaseTimeoutSeats() {
List<Order> orders = orderMapper.selectTimeoutOrders(15);
orders.forEach(order -> {
redisLock.lock(order.getScheduleId());
// 释放座位逻辑...
redisLock.unlock();
});
}
4. 数据库设计优化
4.1 主表结构设计
用户表(user)和车次表(schedule)采用自增主键,但订单表(order)改用雪花算法ID:
java复制@TableId(type = IdType.ASSIGN_ID)
private Long orderId;
这样设计是因为:
- 自增ID在分库分表时会有冲突风险
- 订单ID需要暴露给前端,连续ID可能被恶意爬取
- 雪花ID包含时间信息,便于排查问题
4.2 索引优化
在车次表上建立组合索引:
sql复制CREATE INDEX idx_city_time ON schedule (departure_city, arrival_city, departure_time);
经过EXPLAIN分析,查询性能提升20倍:
code复制| type | key | rows | Extra |
|-------|---------------|------|-------------|
| range | idx_city_time | 23 | Using where |
5. 支付对接实践
5.1 支付流程设计
采用状态机模式管理支付状态:
code复制[待支付] → [支付中] → [支付成功/失败]
关键状态变更代码:
java复制@Transactional
public boolean payOrder(Long orderId) {
Order order = orderMapper.selectByIdForUpdate(orderId); // for update加锁
if (order.getStatus() != 0) {
throw new BusinessException("订单状态异常");
}
order.setStatus(1); // 支付中
orderMapper.updateById(order);
// 调用支付网关...
if (paySuccess) {
order.setStatus(2); // 支付成功
scheduleSeatMapper.updateSeatStatus(order.getScheduleId(),
order.getSeatNo(), 3); // 3表示已售
} else {
order.setStatus(3); // 支付失败
}
return orderMapper.updateById(order) > 0;
}
5.2 支付对账方案
每日凌晨跑批任务处理异常订单:
- 查询支付平台前一日所有交易
- 与本地订单比对,找出状态不一致的
- 自动修复或生成异常报告
6. 安全防护措施
6.1 接口防刷策略
采用令牌桶算法限制高频请求:
java复制@RateLimiter(value = 10, key = "#userId") // 每秒10次
@PostMapping("/query")
public Result querySchedules(...) {
// ...
}
6.2 SQL注入防护
MyBatis-Plus默认使用预编译语句,但需要注意like查询:
java复制// 错误写法
lambdaQuery().like(Schedule::getDepartureCity, "%" + city + "%");
// 正确写法
lambdaQuery().apply("departure_city LIKE CONCAT('%',{0},'%')", city);
7. 部署运维实践
7.1 性能调优
通过Jmeter压测发现两个性能瓶颈:
- 车次查询接口:增加Redis缓存后,QPS从200提升到1500
- 订单创建:去掉不必要的@Transactional,耗时从120ms降到40ms
7.2 监控方案
集成Spring Boot Actuator暴露关键指标:
yaml复制management:
endpoints:
web:
exposure:
include: health,metrics,prometheus
metrics:
export:
prometheus:
enabled: true
配合Grafana监控面板,可以实时查看:
- 接口响应时间
- JVM内存使用
- 数据库连接池状态
8. 典型问题排查
8.1 座位超卖问题
现象:同一座位被两个用户同时购买
原因:Redis锁未覆盖完整事务
解决:将锁粒度扩大到整个购票流程
8.2 支付状态不同步
现象:支付成功但订单仍显示待支付
原因:支付回调接口未做幂等处理
解决:增加幂等校验:
java复制if (order.getStatus() == 2) {
return true; // 已处理直接返回
}
9. 项目扩展方向
- 多级票价:根据时间段动态调整价格
- 联程票:支持中途换乘方案推荐
- 人脸检票:对接车站闸机系统
这套系统在实际部署后,帮助客运企业将线上售票比例从0提升到65%,退票处理时间从30分钟缩短到3分钟。特别提醒:数据库连接池配置要根据实际并发量调整,我们最初使用默认配置导致高峰期出现大量连接超时。