1. 项目背景与需求分析
羽毛球馆作为大众健身的热门场所,传统的人工预约方式存在诸多痛点。每到周末和节假日,电话预约线路拥堵、人工记录容易出错、场地资源分配不均等问题频发。我们团队为本地一家拥有12片标准场地的羽毛球馆开发了这套基于Java的线上自助预约系统,上线后客户预约效率提升300%,管理成本降低60%。
这套系统的核心价值在于:
- 7×24小时不间断服务,突破人工接待的时间限制
- 可视化场地状态展示,用户可直观选择空闲时段
- 自动化冲突检测,彻底解决人工排期容易重复的问题
- 集成微信/支付宝支付,实现预订-支付闭环
- 后台数据统计分析,辅助场馆运营决策
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 技术架构设计
2.1 整体技术栈选型
采用经典的SpringBoot+Vue前后端分离架构:
code复制后端:
- 核心框架:Spring Boot 2.7.5
- 数据库:MySQL 8.0 + Redis 7.0
- 安全框架:Spring Security + JWT
- 其他:Lombok、MyBatis-Plus、Hutool
前端:
- Vue 3 + Element Plus
- ECharts 5.3.2(数据可视化)
- Vant 3(移动端适配)
选择这套技术栈主要基于:
- SpringBoot的自动配置特性大幅减少XML配置
- MyBatis-Plus的ActiveRecord模式简化DAO层开发
- Vue3的Composition API更适合复杂交互场景
- Redis缓存热点数据(如场地状态)减轻数据库压力
2.2 数据库设计关键表
sql复制CREATE TABLE `court` (
`id` int NOT NULL AUTO_INCREMENT,
`name` varchar(20) NOT NULL COMMENT '场地名称',
`type` tinyint NOT NULL COMMENT '1-单打 2-双打',
`status` tinyint DEFAULT '0' COMMENT '0-空闲 1-已预约',
`price` decimal(10,2) NOT NULL COMMENT '每小时价格',
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
CREATE TABLE `booking` (
`id` bigint NOT NULL AUTO_INCREMENT,
`user_id` int NOT NULL,
`court_id` int NOT NULL,
`start_time` datetime NOT NULL,
`end_time` datetime NOT NULL,
`amount` decimal(10,2) NOT NULL,
`pay_status` tinyint DEFAULT '0' COMMENT '0-未支付 1-已支付',
`create_time` datetime DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (`id`),
KEY `idx_user` (`user_id`),
KEY `idx_time` (`start_time`,`end_time`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
特别注意的点:
- 时间字段使用datetime而非timestamp,避免时区问题
- 为预约表建立联合索引提升时间段查询效率
- 金额字段使用decimal而非float,确保精确计算
3. 核心功能实现
3.1 场地状态查询优化
高频访问的场地状态数据采用多级缓存策略:
- 优先从Redis读取(TTL 5分钟)
- 缓存未命中时查询MySQL并回写Redis
- 使用BloomFilter防止缓存穿透
关键代码示例:
java复制@Cacheable(value = "courtStatus", key = "#date.format('yyyyMMdd')")
public List<CourtVO> getCourtStatusByDate(LocalDate date) {
// 1. 检查布隆过滤器
if (!bloomFilter.mightContain(date.toString())) {
return Collections.emptyList();
}
// 2. 查询数据库
LambdaQueryWrapper<Court> wrapper = Wrappers.lambdaQuery();
wrapper.eq(Court::getStatus, 0);
List<Court> courts = courtMapper.selectList(wrapper);
// 3. 转换VO并返回
return courts.stream()
.map(c -> new CourtVO(c.getId(), c.getName(), c.getType()))
.collect(Collectors.toList());
}
3.2 预约冲突检测
采用时间重叠算法确保同一场地不会重复预约:
java复制public boolean checkTimeConflict(Integer courtId, LocalDateTime start, LocalDateTime end) {
Integer count = bookingMapper.selectCount(
Wrappers.<Booking>lambdaQuery()
.eq(Booking::getCourtId, courtId)
.and(w -> w.between(Booking::getStartTime, start, end)
.or()
.between(Booking::getEndTime, start, end))
);
return count > 0;
}
3.3 支付流程设计
支付状态机保证交易一致性:
code复制待支付 --[用户支付]--> 支付中 --[支付成功]--> 已支付
|--[支付失败]--> 已取消
|--[超时未支付]--> 已失效
使用Spring StateMachine实现状态流转:
java复制@Configuration
@EnableStateMachine
public class PaymentStateMachineConfig
extends EnumStateMachineConfigurerAdapter<PaymentState, PaymentEvent> {
@Override
public void configure(StateMachineStateConfigurer<PaymentState, PaymentEvent> states)
throws Exception {
states.withStates()
.initial(PaymentState.UNPAID)
.states(EnumSet.allOf(PaymentState.class));
}
@Override
public void configure(StateMachineTransitionConfigurer<PaymentState, PaymentEvent> transitions)
throws Exception {
transitions
.withExternal()
.source(PaymentState.UNPAID)
.target(PaymentState.PAYING)
.event(PaymentEvent.PAY_REQUEST)
.and()
.withExternal()
.source(PaymentState.PAYING)
.target(PaymentState.PAID)
.event(PaymentEvent.PAY_SUCCESS);
}
}
4. 性能优化实践
4.1 高并发场景处理
采用分级锁策略:
- 乐观锁处理余额变更
- 分布式锁控制库存扣减
- 本地缓存减少Redis访问
压测指标对比:
| 优化措施 | QPS | 平均响应时间 |
|---|---|---|
| 无优化 | 120 | 450ms |
| 加Redis缓存 | 800 | 120ms |
| 加本地缓存 | 1500 | 65ms |
| 最终方案 | 2100 | 38ms |
4.2 SQL优化案例
原始查询:
sql复制SELECT * FROM booking
WHERE court_id = ?
AND DATE(start_time) = ?
优化后:
sql复制SELECT * FROM booking
WHERE court_id = ?
AND start_time >= ?
AND start_time < ? + INTERVAL 1 DAY
执行计划对比:
- 原始:全表扫描,耗时320ms
- 优化:索引范围查询,耗时12ms
5. 安全防护措施
5.1 防刷单机制
- 同一IP限流:Guava RateLimiter
- 业务规则限制:
- 单用户最大同时预约数:3个
- 最短预约间隔:30分钟
- 验证码策略:
- 普通操作:图形验证码
- 支付环节:短信验证码
5.2 数据安全
- 敏感字段加密:
java复制@ColumnTransformer( read = "AES_DECRYPT(UNHEX(user_mobile), '${aes.key}')", write = "HEX(AES_ENCRYPT(?, '${aes.key}'))" ) private String mobile; - 日志脱敏处理:
java复制@JsonSerialize(using = SensitiveSerializer.class) private String idCardNo;
6. 部署与监控
6.1 容器化部署
Docker Compose编排方案:
yaml复制version: '3'
services:
app:
image: openjdk:17-jdk
ports:
- "8080:8080"
volumes:
- ./logs:/app/logs
environment:
- SPRING_PROFILES_ACTIVE=prod
mysql:
image: mysql:8.0
environment:
- MYSQL_ROOT_PASSWORD=123456
volumes:
- ./mysql-data:/var/lib/mysql
redis:
image: redis:7.0
ports:
- "6379:6379"
6.2 监控方案
- Spring Boot Actuator暴露健康检查
- Prometheus + Grafana监控看板
- ELK日志收集系统
- 关键业务指标:
- 预约成功率
- 支付转化率
- 接口响应P99
7. 踩坑与解决方案
7.1 时区问题
现象:预约时间显示比实际晚8小时
原因:MySQL默认时区与系统时区不一致
解决:
yaml复制spring:
datasource:
url: jdbc:mysql://localhost:3306/booking?useSSL=false&serverTimezone=Asia/Shanghai
7.2 分布式锁失效
现象:Redis锁在集群环境下偶尔失效
原因:主从切换导致锁丢失
解决方案:采用RedLock算法
java复制public boolean tryLock(String lockKey, long expireTime) {
List<RedisNode> nodes = redisClusterConfiguration.getClusterNodes();
int successCount = 0;
for (RedisNode node : nodes) {
Jedis jedis = new Jedis(node.getHost(), node.getPort());
if ("OK".equals(jedis.set(lockKey, "1", "NX", "PX", expireTime))) {
successCount++;
}
}
return successCount > (nodes.size() / 2);
}
8. 扩展功能展望
- 智能推荐:基于用户历史预约数据推荐合适时段
- 人脸识别签到:对接第三方SDK实现无接触入场
- 动态调价:根据供需关系自动调整场地价格
- 装备租赁:线上预约羽毛球拍等器材
这套系统经过半年实际运行,日均处理预约请求超过2000次,系统可用性达到99.99%。核心经验是:对于体育场馆这类传统行业,技术赋能的关键在于理解业务场景的真实痛点,用稳定可靠的技术方案解决具体问题,而非盲目追求新技术。
