1. 项目背景与核心需求
旅游行业数字化进程加速的当下,景区门票与酒店预订系统的在线化已成为刚需。传统线下购票方式存在排队时间长、信息不透明、资源调配效率低等问题,而分散的预订平台又导致用户体验割裂。这个基于SpringBoot+Vue的全栈系统正是为解决这些痛点而生。
我曾参与过多个省级文旅平台的开发,发现真正好用的预订系统需要同时满足三个核心诉求:
- 游客侧:一站式完成"查景点-买门票-订酒店"全流程操作
- 管理侧:实时掌握库存与订单数据,动态调整资源分配
- 架构侧:应对节假日流量高峰时的系统稳定性
本系统采用前后端分离架构,前端用Vue3+Element Plus实现响应式界面,后端基于SpringBoot 2.7提供RESTful API,数据库选用MySQL 8.0并配合Redis缓存。这种技术组合在保证功能完整性的同时,兼具良好的扩展性和维护性。
关键设计原则:将门票库存管理、酒店房态控制等核心业务逻辑放在后端实现,前端只负责展示和交互,避免业务规则泄露到客户端。
2. 技术架构设计详解
2.1 后端SpringBoot核心模块
采用经典的三层架构设计,各层职责明确:
code复制com.tourism
├── config # 配置类
├── controller # 对外接口
├── service # 业务逻辑
│ ├── impl # 实现类
├── dao # 数据访问
├── entity # 实体类
├── dto # 数据传输对象
├── util # 工具类
└── exception # 异常处理
重点说明几个关键实现:
分布式锁控制库存
java复制// 使用Redisson实现分布式锁
public boolean reduceTicketStock(Long attractionId, Integer quantity) {
RLock lock = redissonClient.getLock("lock:stock:" + attractionId);
try {
if (lock.tryLock(5, 10, TimeUnit.SECONDS)) {
Attraction attraction = attractionMapper.selectById(attractionId);
if (attraction.getRemainStock() >= quantity) {
attraction.setRemainStock(attraction.getRemainStock() - quantity);
return attractionMapper.updateById(attraction) > 0;
}
}
} finally {
lock.unlock();
}
return false;
}
定时任务同步房态
java复制@Scheduled(cron = "0 0 2 * * ?") // 每天凌晨2点执行
public void syncRoomStatus() {
List<Hotel> hotels = hotelMapper.selectList(null);
hotels.forEach(hotel -> {
// 调用第三方PMS接口同步房态
RoomStatusDTO status = pmsClient.getRoomStatus(hotel.getCode());
hotel.setAvailableRooms(status.getAvailable());
hotelMapper.updateById(hotel);
});
}
2.2 前端Vue工程结构
采用Vue CLI创建的工程标准结构,关键改进点:
code复制src/
├── api/ # 接口请求封装
├── assets/ # 静态资源
├── components/ # 公共组件
│ ├── Calendar/ # 日期选择器
│ ├── Map/ # 景区地图
│ └── ...
├── router/ # 路由配置
├── store/ # Vuex状态管理
├── utils/ # 工具函数
├── views/ # 页面组件
│ ├── Attraction/ # 景区相关
│ ├── Hotel/ # 酒店相关
│ ├── Order/ # 订单相关
│ └── ...
└── main.js # 入口文件
性能优化实践:
- 路由懒加载:
const AttractionDetail = () => import('./views/Attraction/Detail.vue') - 接口请求节流:使用lodash的throttle包装高频操作
- 图片懒加载:
<img v-lazy="imageUrl">配合vue-lazyload插件
3. 核心功能实现细节
3.1 景区门票预订流程
采用状态机模式管理订单生命周期:
mermaid复制stateDiagram
[*] --> PENDING
PENDING --> PAID: 支付成功
PENDING --> CANCELLED: 用户取消
PAID --> USED: 核销入园
PAID --> REFUNDING: 申请退款
REFUNDING --> REFUNDED: 退款成功
REFUNDING --> PAID: 退款驳回
关键代码实现:
java复制public enum OrderStatus {
PENDING("待支付", Arrays.asList(Event.PAY, Event.CANCEL)),
PAID("已支付", Arrays.asList(Event.USE, Event.APPLY_REFUND)),
// 其他状态定义...
public boolean canTransferTo(Event event) {
return allowedEvents.contains(event);
}
}
3.2 酒店房态管理算法
解决超卖问题的核心逻辑:
- 实时房态缓存:使用Redis Hash存储每日房态
code复制HSET hotel:2024-07-20 101 0 # 房号101,0表示可售 - 预占机制:创建订单时先标记为"预占"状态,15分钟内未支付自动释放
- 库存回补:每晚执行Job释放过期预占
3.3 支付对接方案
采用策略模式支持多种支付方式:
java复制public interface PaymentStrategy {
PaymentResult pay(Order order);
}
@Service
@RequiredArgsConstructor
public class PaymentService {
private final Map<String, PaymentStrategy> strategies;
public PaymentResult handlePayment(String type, Order order) {
PaymentStrategy strategy = strategies.get(type + "Strategy");
if (strategy == null) {
throw new UnsupportedPaymentException();
}
return strategy.pay(order);
}
}
已实现:
- 支付宝支付(App支付+网页支付)
- 微信支付(JSAPI+H5)
- 银联云闪付
4. 高并发场景应对策略
4.1 缓存设计实践
采用多级缓存架构:
- 本地缓存(Caffeine):存储热点景区信息,TTL=5分钟
- 分布式缓存(Redis):
- 门票库存:
INCRBY attraction:1:stock -1 - 价格日历:ZSET结构存储30天内价格
- 门票库存:
- 缓存击穿防护:
java复制public Attraction getAttractionWithCache(Long id) {
String key = "attraction:" + id;
Attraction attraction = redisTemplate.opsForValue().get(key);
if (attraction == null) {
synchronized (this) {
attraction = redisTemplate.opsForValue().get(key);
if (attraction == null) {
attraction = attractionMapper.selectById(id);
redisTemplate.opsForValue().set(key, attraction, 30, TimeUnit.MINUTES);
}
}
}
return attraction;
}
4.2 限流与降级方案
- Sentinel配置:
yaml复制spring:
cloud:
sentinel:
transport:
dashboard: localhost:8080
datasource:
ds1:
nacos:
server-addr: localhost:8848
dataId: sentinel-rules
rule-type: flow
-
核心接口限流规则:
- 门票查询:1000 QPS
- 下单接口:500 QPS
- 支付回调:300 QPS
-
降级策略:
- 当景区详情服务不可用时,返回缓存中的精简信息
- 支付系统超时后引导用户稍后重试
5. 安全防护体系
5.1 常见攻击防御
-
XSS防护:
- 前端:vue-dompurify-html插件净化HTML
- 后端:Jackson配置HTML转义
java复制@Bean public Jackson2ObjectMapperBuilder objectMapperBuilder() { return new Jackson2ObjectMapperBuilder() .defaultHtmlEscaping(true); } -
CSRF防护:
- 前后端分离架构下使用JWT+自定义请求头
- 关键操作要求二次验证(短信/邮件)
-
SQL注入:
- 强制使用MyBatis参数绑定
xml复制<select id="selectById" resultType="Attraction"> SELECT * FROM attraction WHERE id = #{id} </select>
5.2 数据加密方案
-
敏感字段加密:
java复制@Column(columnDefinition = "varchar(255)") @Convert(converter = CryptoConverter.class) private String idCardNumber; -
传输层加密:
- 全站HTTPS(TLS 1.3)
- 敏感接口额外使用RSA加密报文
-
密码存储:
java复制public String encryptPassword(String raw) { return new BCryptPasswordEncoder().encode(raw); }
6. 运维监控体系
6.1 日志收集方案
-
ELK Stack配置:
yaml复制logging: file: path: /var/log/tourism logstash: enabled: true host: logstash.example.com port: 5044 -
关键业务日志标记:
java复制@Slf4j @RestController @RequestMapping("/order") public class OrderController { @PostMapping public Result createOrder(@Valid @RequestBody OrderDTO dto) { log.info("[ORDER-CREATE] user:{}, items:{}", SecurityUtils.getUserId(), dto.getItems()); // 业务逻辑 } }
6.2 性能监控指标
-
Prometheus监控项:
- 接口响应时间(p99<500ms)
- 数据库连接池使用率(<80%)
- JVM内存占用(Old Gen<70%)
-
Grafana看板配置:
- 实时订单量
- 支付成功率
- 库存变更趋势
-
业务指标报警:
- 门票售罄预警
- 异常退款率突增
- 同一账号频繁取消
7. 项目部署实践
7.1 容器化部署
Docker Compose编排方案:
yaml复制version: '3.8'
services:
mysql:
image: mysql:8.0
environment:
MYSQL_ROOT_PASSWORD: ${DB_PASSWORD}
volumes:
- mysql_data:/var/lib/mysql
redis:
image: redis:6.2
command: redis-server --requirepass ${REDIS_PASSWORD}
backend:
build: ./backend
ports:
- "8080:8080"
depends_on:
- mysql
- redis
frontend:
build: ./frontend
ports:
- "80:80"
volumes:
mysql_data:
7.2 CI/CD流程
GitLab Runner配置示例:
yaml复制stages:
- test
- build
- deploy
backend-test:
stage: test
script:
- cd backend
- mvn test
frontend-build:
stage: build
script:
- cd frontend
- npm install
- npm run build
artifacts:
paths:
- frontend/dist
backend-deploy:
stage: deploy
script:
- scp target/*.jar user@server:/app
- ssh user@server "systemctl restart tourism"
8. 典型问题排查实录
8.1 库存超卖问题
现象:促销活动期间出现同一门票被重复售卖
排查过程:
- 检查日志发现多个线程同时通过库存校验
- 确认数据库隔离级别为READ_COMMITTED
- 发现应用服务器时钟不同步导致Redis锁失效
解决方案:
- 引入Redlock算法实现分布式锁
- 增加数据库乐观锁版本号
sql复制UPDATE attraction SET remain_stock = remain_stock - 1, version = version + 1 WHERE id = ? AND version = ?
8.2 支付回调丢失
现象:部分用户已付款但订单状态未更新
排查过程:
- 检查支付平台日志确认回调已发送
- 发现Nginx限制post body大小为1M
- 某些回调报文因包含详细账单超过限制
解决方案:
- 调整Nginx配置:
nginx复制client_max_body_size 5M; - 增加补偿查询机制:每小时扫描已支付未确认订单
9. 扩展优化方向
9.1 智能推荐模块
基于用户行为的推荐策略:
- 协同过滤:
用户A看了X景点 → 推荐其他用户看过X后看的Y景点 - 内容相似:
故宫 → 颐和园(同属历史古迹类) - 实时热点:
近期搜索"漂流" → 推送周边漂流景点
技术实现:
python复制# 使用TensorFlow实现简单推荐模型
model = tf.keras.Sequential([
tf.keras.layers.Embedding(num_attractions, 16),
tf.keras.layers.GlobalAveragePooling1D(),
tf.keras.layers.Dense(16, activation='relu'),
tf.keras.layers.Dense(num_attractions, activation='softmax')
])
9.2 可视化数据分析
使用ECharts实现的经营看板:
- 实时游客来源地热力图
- 门票销售趋势预测
- 酒店入住率日历视图
关键技术点:
- WebSocket实时推送数据更新
- 大数据量下采用分片加载策略
- 移动端适配方案
在项目实际落地过程中,最大的挑战不是技术实现,而是平衡业务灵活性与系统稳定性。比如景区临时调整开放时间、酒店突然维修部分房间等场景,需要设计足够灵活的配置机制,同时保证这些变更不会破坏已有的订单契约。我的经验是:核心业务规则要硬编码保证确定性,非核心属性则通过管理后台可配置。
