民宿预订系统优惠券营销模块设计要点
一、业务痛点:民宿优惠券场景下的技术难题
民宿预订和通用电商订单存在明显差异,房源绑定、入住时段限制、多维度核销规则,让优惠券模块更容易出现业务漏洞。
- 领券并发超发:大促活动时大量用户同时领取优惠券,简单的先查询再插入会造成超领,超出营销预算。
- 优惠规则校验复杂:优惠券需要绑定指定民宿、限定入住日期、设置最低入住晚数、订单满额门槛,多条件叠加容易出现价格计算错误。
- 核销幂等难以保障:同一订单重复提交优惠券,会导致多次扣减优惠金额,造成资金损失。
- 优惠券与房态联动:淡季券、周末专享券需要和房源日历强绑定,未做时段校验会出现违规使用。

二、整体架构设计
本模块采用SpringBoot+MySQL+Redis技术栈,分层设计解耦业务逻辑,整体分为四层:
- 接口层:提供领券、查券、核销接口,做参数校验与防重复提交。
- 业务服务层:拆分领券服务、规则校验服务、核销服务,使用策略模式处理不同类型优惠券。
- 缓存层:Redis承载优惠券库存、用户领券记录,拦截大部分数据库请求,应对高并发领取场景。
- 数据持久层:MySQL存储优惠券批次、用户持有券、核销记录,保证数据可追溯。

三、数据库模型设计
3.1 核心数据表设计
- 优惠券批次表(coupon_batch):存储券的基础规则,包含优惠类型、面额、有效期、适用民宿、入住时段限制、总库存、单人限领数量。
- 用户优惠券表(user_coupon):记录用户领取到的每一张券,包含券状态(未使用/已使用/已过期)、绑定订单号、核销时间。
- 优惠券核销记录表(coupon_verify_log):保存每一次核销流水,保证订单核销可追溯,防止重复核销。
3.2 关键字段约束
为防止超领与重复领券,在user_coupon表建立userId+batchId联合唯一索引,作为最后的数据兜底。
四、高并发领券功能实现(Java代码实现)
4.1 三级防超领方案
- 前端:按钮置灰,限制短时间重复点击。
- Redis层:使用分布式锁控制单人单次领券,同时缓存剩余库存,避免穿透数据库。
- 数据库层:联合唯一索引+行锁,杜绝重复领取。
4.2 核心领券代码
@Service
public class CouponReceiveService {
@Autowired
private StringRedisTemplate redisTemplate;
@Autowired
private CouponBatchMapper batchMapper;
@Autowired
private UserCouponMapper userCouponMapper;
// 分布式锁前缀
private static final String LOCK_PREFIX = "coupon:lock:";
// 库存缓存key
private static final String STOCK_PREFIX = "coupon:stock:";
@Transactional(rollbackFor = Exception.class)
public ResultDTO receiveCoupon(Long userId, Long batchId) {
String lockKey = LOCK_PREFIX + userId + ":" + batchId;
// 1. 获取Redis分布式锁
Boolean lock = redisTemplate.opsForValue().setIfAbsent(lockKey, "1", 30, TimeUnit.SECONDS);
if (!lock) {
return ResultDTO.fail("请勿重复领取优惠券");
}
try {
// 2. 校验缓存库存
String stockStr = redisTemplate.opsForValue().get(STOCK_PREFIX + batchId);
if (StringUtils.isEmpty(stockStr) || Integer.parseInt(stockStr) <= 0) {
return ResultDTO.fail("优惠券已领完");
}
// 3. 扣减缓存库存
Long decrement = redisTemplate.opsForValue().decrement(STOCK_PREFIX + batchId);
if (decrement < 0) {
redisTemplate.opsForValue().increment(STOCK_PREFIX + batchId);
return ResultDTO.fail("优惠券已领完");
}
// 4. 写入用户优惠券记录,唯一索引防止重复入库
UserCoupon userCoupon = new UserCoupon();
userCoupon.setUserId(userId);
userCoupon.setBatchId(batchId);
userCoupon.setStatus(0);
userCouponMapper.insert(userCoupon);
return ResultDTO.success("领券成功");
} finally {
// 释放分布式锁
redisTemplate.delete(lockKey);
}
}
}
五、民宿场景专属规则校验模块设计
民宿优惠券不能直接套用电商满减逻辑,必须增加房源与入住时段校验,采用策略模式区分不同券类型:满减券、折扣券、周末专享券、连住专享券。
5.1 核心校验规则
- 房源范围校验:优惠券仅支持指定民宿,过滤掉不在范围内的房源订单。
- 入住日期校验:判断订单入住时间是否在券的可用时段内,屏蔽节假日不可用限制。
- 入住晚数校验:满足最低连住晚数才能使用优惠券。
- 订单金额门槛:订单金额达到最低消费金额才可核销。
5.2 规则校验核心代码
public interface CouponRuleStrategy {
/**
* 校验优惠券是否可用
* @param userCoupon 用户券
* @param orderDTO 预订订单信息
* @return 校验结果
*/
CheckResult check(UserCoupon userCoupon, HomestayOrderDTO orderDTO);
}
// 连住优惠券实现类
@Component
public class NightCouponStrategy implements CouponRuleStrategy {
@Override
public CheckResult check(UserCoupon userCoupon, HomestayOrderDTO orderDTO) {
CouponBatch batch = batchMapper.selectById(userCoupon.getBatchId());
// 校验入住晚数
if (orderDTO.getNightCount() < batch.getMinNights()) {
return CheckResult.fail("未达到最低入住晚数要求");
}
// 校验民宿匹配
if (!batch.getHomestayIdList().contains(orderDTO.getHomestayId())) {
return CheckResult.fail("该优惠券不适用于当前民宿");
}
return CheckResult.success();
}
}
六、订单核销与幂等控制
6.1 核销业务逻辑
- 下单时传入券ID,先执行全部规则校验;
- 校验通过后,使用数据库乐观锁更新用户优惠券状态为已使用;
- 插入核销流水,绑定订单ID,保证一张券只能绑定一个订单。
6.2 幂等实现要点
- 接口增加唯一流水号,重复请求直接返回上次处理结果;
- 更新优惠券状态时带上版本号,防止多次更新;
- 核销流水表增加订单ID唯一索引,避免同一订单多次核销优惠券。
七、异常兜底与数据一致性保障
- 缓存与数据库库存不一致:定时任务定期将Redis库存和数据库批次库存做校对,自动补偿差额。
- 领券失败回滚:库存扣减成功但入库失败时,主动回滚Redis库存数值。
- 过期券自动处理:通过定时任务批量更新已过期优惠券状态,释放占用的营销额度。

八、模块性能优化总结
- 热点优惠券库存全部放入Redis缓存,减少MySQL查询压力;
- 规则校验逻辑全部下沉到业务层,避免复杂SQL拖慢查询;
- 用户领券记录按用户ID做分表,随着用户量增长平滑扩容;
- 异步记录核销日志,不阻塞主订单下单流程。
九、结语
民宿优惠券模块的核心难点,不在于简单的金额计算,而是贴合民宿预订的房源、入住时段、连住条件等业务约束,同时兼顾高并发领券下的数据安全。通过缓存+分布式锁+数据库唯一索引三层防护,配合策略模式管理多样化优惠规则,既可以支撑大促峰值流量,又能严格控制营销成本,避免超发、错用、重复核销等线上问题。