1. 项目概述与核心价值
这个火车票订票系统是我在毕业设计期间完成的一个全栈项目,采用SpringBoot+Vue+MySQL技术栈实现。作为一个完整的商业系统简化版,它包含了用户注册登录、车次查询、余票显示、订单管理、支付模拟等核心功能模块。不同于教学演示项目,我在设计中特别注重了以下几个实际业务场景的模拟:
- 高并发场景下的余票计算(避免超卖)
- 分布式场景下的座位锁定机制
- 前后端分离架构下的接口设计规范
- 移动端与PC端的响应式适配
整套系统代码量约1.2万行,包含58个Java类、32个Vue组件和28张数据表。在本地测试环境下,单服务器可支撑约800QPS的并发查询请求,订单创建处理的平均响应时间控制在300ms以内。
提示:这个项目最值得参考的价值在于它完整呈现了一个可落地的分布式事务解决方案——如何在不使用重量级中间件的情况下,通过应用层逻辑保证"查询-锁定-支付"这个业务流程的数据一致性。
2. 技术架构设计解析
2.1 后端SpringBoot架构
采用经典的三层架构设计,但针对票务系统特点做了特殊优化:
code复制com.ticket
├── config # 安全、缓存等配置
├── controller # 按业务域划分的REST接口
├── service # 核心业务逻辑
│ ├── impl # 实现类
│ └── strategy # 策略模式实现不同业务规则
├── repository # JPA数据访问层
├── model # 实体与DTO
└── util # 工具类
特别值得说明的是SeatLockService这个核心服务类,它通过组合使用:
- Redis分布式锁(Redisson实现)
- 数据库乐观锁(version字段)
- 本地Guava缓存
实现了座位资源的并发控制。具体加锁逻辑如下:
java复制public boolean lockSeat(Long scheduleId, String seatNo) {
String lockKey = "seat:" + scheduleId + ":" + seatNo;
RLock lock = redissonClient.getLock(lockKey);
try {
if (lock.tryLock(3, 10, TimeUnit.SECONDS)) {
// 检查余票并锁定
int updated = ticketMapper.lockSeat(scheduleId, seatNo);
return updated > 0;
}
} finally {
lock.unlock();
}
return false;
}
2.2 前端Vue工程结构
基于Vue CLI 4.x搭建,采用如下模块划分:
code复制src/
├── api/ # Axios接口封装
├── assets/ # 静态资源
├── components/ # 公共组件
│ ├── common/ # 基础UI组件
│ └── ticket/ # 票务业务组件
├── router/ # 路由配置
├── store/ # Vuex状态管理
├── utils/ # 工具函数
└── views/ # 页面视图
在性能优化方面主要做了:
- 路由懒加载
- 高频查询结果缓存到localStorage
- 使用Virtual List优化长列表渲染
车次查询页面的核心组件TrainList.vue中,我采用了"查询条件-结果列表"的经典布局,并加入了以下业务特性:
vue复制<template>
<div class="query-container">
<el-form @submit.native.prevent="handleQuery">
<!-- 出发/到达站选择 -->
<StationSelector
v-model="queryParams.departure"
placeholder="出发站" />
<!-- 日期选择器 -->
<el-date-picker
v-model="queryParams.date"
type="date"
:disabled-date="disablePastDate"
value-format="yyyy-MM-dd"
/>
<el-button
type="primary"
native-type="submit"
:loading="loading">
查询车次
</el-button>
</el-form>
<!-- 查询结果表格 -->
<TrainTable
:data="trainList"
@row-click="handleSelectTrain" />
</div>
</template>
3. 数据库设计与优化
3.1 核心表结构
系统共设计28张表,以下是5个最核心的表结构:
- 用户表(user)
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) NOT NULL COMMENT '加密盐值',
`real_name` varchar(50) DEFAULT NULL COMMENT '真实姓名',
`id_card` varchar(18) DEFAULT NULL COMMENT '身份证号',
`phone` varchar(11) DEFAULT NULL COMMENT '手机号',
`avatar` varchar(255) DEFAULT NULL COMMENT '头像URL',
`status` tinyint(4) NOT NULL DEFAULT '1' COMMENT '状态:0-禁用 1-正常',
`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='用户表';
- 车次表(train)
sql复制CREATE TABLE `train` (
`id` bigint(20) NOT NULL AUTO_INCREMENT,
`code` varchar(10) NOT NULL COMMENT '车次号如G123',
`type` varchar(10) NOT NULL COMMENT '车型:G-高铁 D-动车 K-快速',
`start_station` varchar(50) NOT NULL,
`end_station` varchar(50) NOT NULL,
`start_time` time NOT NULL COMMENT '始发时间',
`end_time` time NOT NULL COMMENT '终到时间',
`duration` int(11) NOT NULL COMMENT '运行时长(分钟)',
`status` tinyint(4) NOT NULL DEFAULT '1' COMMENT '状态:0-停运 1-正常',
PRIMARY KEY (`id`),
UNIQUE KEY `idx_code` (`code`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='车次基础信息';
3.2 查询性能优化
针对票务系统的高频查询场景,主要采取了以下优化措施:
-
余票缓存策略:
- 使用Redis Hash存储每个车次不同席别的余票数
- 设置5秒自动过期 + 查询时自动续期
- 数据库变更时通过@TransactionalEventListener异步更新缓存
-
联合索引设计:
sql复制-- 车次时刻表查询优化
ALTER TABLE `train_schedule`
ADD INDEX `idx_departure_time` (`departure_station_id`, `arrival_station_id`, `departure_time`);
-- 订单查询优化
ALTER TABLE `order`
ADD INDEX `idx_user_status` (`user_id`, `status`, `create_time`);
- 分表策略:
- 订单表按月分表(order_202301, order_202302...)
- 通过MyBatis拦截器实现动态表名路由
- 历史数据归档到单独的归档库
4. 关键业务逻辑实现
4.1 订票流程时序
完整的订票业务流程涉及多个服务的协作:
mermaid复制sequenceDiagram
participant User
participant Frontend
participant API
participant OrderService
participant TicketService
participant PaymentService
User->>Frontend: 选择车次/座位
Frontend->>API: POST /api/orders (创建订单)
API->>OrderService: 生成订单(待支付状态)
OrderService->>TicketService: 锁定座位
TicketService-->>OrderService: 锁定结果
OrderService-->>API: 订单创建结果
API-->>Frontend: 返回订单详情
Frontend->>User: 显示支付页面
User->>Frontend: 确认支付
Frontend->>API: POST /api/payments (发起支付)
API->>PaymentService: 处理支付
PaymentService-->>API: 支付结果
API->>OrderService: 更新订单状态
OrderService-->>API: 更新结果
API-->>Frontend: 支付结果
Frontend->>User: 显示订票成功
注意:实际代码中需要处理各种异常情况,比如:
- 座位锁定失败时的订单自动取消
- 支付超时后的座位释放
- 重复支付的处理等
4.2 余票计算算法
余票计算是本系统的核心难点,主要考虑以下因素:
- 不同区间段的座位复用(如A->C的车次,B站上车的人不影响A->B的余票)
- 多种席别的独立计算(商务座、一等座、二等座)
- 预留票(铁路部门保留的机动票)
核心计算逻辑如下:
java复制public List<SeatInventory> calculateAvailableSeats(
Long trainId,
Long fromStationId,
Long toStationId) {
// 1. 获取该车次所有区间的已售座位
List<SeatSale> sales = seatSaleMapper.selectByTrain(trainId);
// 2. 获取该车次所有座位的基础信息
List<TrainSeat> seats = trainSeatMapper.selectByTrain(trainId);
// 3. 按席别分组统计
Map<String, List<TrainSeat>> seatMap = seats.stream()
.collect(Collectors.groupingBy(TrainSeat::getSeatType));
// 4. 计算每个席别的可用座位
List<SeatInventory> result = new ArrayList<>();
seatMap.forEach((type, seatList) -> {
long total = seatList.size();
long sold = sales.stream()
.filter(s -> s.getSeatType().equals(type))
.filter(s -> isConflict(s, fromStationId, toStationId))
.count();
long available = total - sold - getReservedCount(type);
result.add(new SeatInventory(type, available, total));
});
return result;
}
private boolean isConflict(SeatSale sale, Long fromId, Long toId) {
return !(sale.getToStationId() <= fromId || sale.getFromStationId() >= toId);
}
5. 部署方案与性能调优
5.1 本地开发环境部署
最小化开发环境需要以下组件:
- 后端服务:
- JDK 1.8+
- Maven 3.6+
- MySQL 5.7+
- Redis 5.0+
启动步骤:
bash复制# 克隆项目
git clone https://github.com/example/ticket-system.git
# 初始化数据库
mysql -u root -p < docs/sql/init.sql
# 启动后端
cd ticket-server
mvn spring-boot:run -Dspring.profiles.active=dev
- 前端服务:
- Node.js 14+
- npm 6+
启动步骤:
bash复制cd ticket-web
npm install
npm run serve
5.2 生产环境部署建议
对于中小规模的线上环境,推荐以下配置:
| 组件 | 配置示例 | 说明 |
|---|---|---|
| 应用服务器 | 2核4G × 2台 | 建议至少两台做负载均衡 |
| MySQL | 4核8G,SSD磁盘 | 主从架构,开启binlog |
| Redis | 2核4G,持久化开启 | 哨兵模式保证高可用 |
| Nginx | 1核2G | 做静态资源服务和反向代理 |
关键JVM参数配置示例:
code复制-server -Xms2g -Xmx2g -XX:MetaspaceSize=256m
-XX:MaxMetaspaceSize=256m -XX:+UseG1GC
-XX:MaxGCPauseMillis=200
5.3 性能测试数据
使用JMeter进行压力测试的结果:
| 场景 | 线程数 | 平均响应时间 | 吞吐量 | 错误率 |
|---|---|---|---|---|
| 车次查询 | 100 | 68ms | 1250/s | 0% |
| 余票查询 | 50 | 120ms | 420/s | 0% |
| 下单接口 | 30 | 280ms | 110/s | 0.2% |
| 支付接口 | 20 | 350ms | 60/s | 0.5% |
优化建议:
- 余票查询添加二级缓存(本地缓存+Redis)
- 订单创建接口采用异步处理+结果轮询
- 支付结果回调使用消息队列削峰
6. 毕业设计扩展建议
如果希望进一步提升项目质量,可以考虑以下扩展方向:
-
微服务化改造:
- 将系统拆分为用户服务、票务服务、订单服务、支付服务
- 使用Spring Cloud Alibaba全家桶
- 引入Sentinel实现熔断降级
-
大数据分析模块:
- 使用Flink实时分析订票数据
- 生成热门线路、高峰时段等统计报表
- 基于历史数据预测余票紧张度
-
移动端适配:
- 使用Uniapp开发跨平台移动应用
- 加入扫码乘车等实用功能
- 实现离线模式下的车次缓存查询
-
安全加固:
- 接口签名验证
- 敏感数据加密
- 操作日志审计
我在实际开发过程中最大的体会是:分布式系统中的状态一致性是最难处理的,特别是在没有使用分布式事务框架的情况下。最终我采用的解决方案是"状态机+补偿机制",通过一个专门的OrderStateMachine类来管理订单状态流转,任何异常情况都会触发预定义的补偿动作。这种设计虽然增加了代码复杂度,但保证了系统在各种异常情况下都能保持数据一致性。
