电商优惠券系统怎么设计?一次讲清领券、用券、叠加规则、核销与风控思路
大家好,我是一名有 4 年工作经验的 Java 后端开发。
优惠券系统在电商里看起来像营销模块,但真正做起来会发现,它几乎把状态、并发、规则、风控、资金口径全都串到了一起。
这篇文章我想系统聊一聊优惠券系统到底应该怎么设计。
🦅个人主页
🐼
文章目录
- 电商优惠券系统怎么设计?一次讲清领券、用券、叠加规则、核销与风控思路
- 一、前言
- 二、核心模型怎么拆
- 2.1 券模板
- 2.2 用户券
- 2.3 券使用记录
- 三、推荐状态设计
- 四、最关键的几个问题
- 4.1 怎么防止重复领券
- 4.2 怎么防止超发
- 4.3 下单时怎么选券
- 4.4 为什么要有锁券状态
- 五、数据库示例
- 5.1 券模板表
- 5.2 用户券表
- 5.3 用户券流水表
- 六、最容易踩的坑
- 6.1 把券模板和用户券混在一起
- 6.2 没有锁券状态
- 6.3 规则计算和发券逻辑耦合太深
- 6.4 只考虑领券,不考虑回滚和核销
- 七、面试中怎么回答
- 八、总结
- 九、结尾
一、前言
优惠券系统常见的问题包括:
- 用户重复领券
- 券被超发
- 下单时算券很慢
- 多张券叠加规则复杂
- 支付失败后券状态回滚不一致
- 活动结束后数据对不上
所以优惠券系统真正要解决的,不只是“发几张券”,而是:
规则配置、领券控制、用券核销、状态流转和风控约束的完整闭环。
二、核心模型怎么拆
我更建议至少拆成三层:
2.1 券模板
描述规则:
- 满减 / 折扣 / 直减
- 使用门槛
- 生效时间
- 叠加规则
- 适用范围
2.2 用户券
描述实例:
- 归属用户
- 当前状态
- 领取时间
- 使用时间
2.3 券使用记录
描述动作:
- 何时领取
- 何时锁定
- 何时核销
- 何时回滚
三、推荐状态设计
用户券通常建议至少有这些状态:
INITAVAILABLELOCKEDUSEDEXPIREDINVALID
其中:
- 下单提交时可以先
LOCKED - 支付成功后变
USED - 订单取消或支付失败再从
LOCKED回滚成AVAILABLE
四、最关键的几个问题
4.1 怎么防止重复领券
常见做法:
- 用户券表唯一索引
- 领券接口幂等
- Redis 前置限流 / 防重
4.2 怎么防止超发
常见做法:
- 券模板库存字段
- 条件更新扣库存
- 活动高峰可加 Redis 预扣减
4.3 下单时怎么选券
核心思路通常是:
- 先筛掉不满足门槛和范围的券
- 再根据叠加规则过滤
- 再从剩余券里挑最优组合
如果规则复杂,最好有独立的:
- 优惠计算服务
4.4 为什么要有锁券状态
因为用户提交订单后,不一定马上支付成功。
这时候券已经参与了订单计算,但又不能立即真正核销,所以通常要有:
LOCKED
支付成功后:
LOCKED -> USED
支付失败或取消后:
LOCKED -> AVAILABLE
五、数据库示例
5.1 券模板表
CREATETABLEcoupon_template(idBIGINTPRIMARYKEYAUTO_INCREMENT,nameVARCHAR(64)NOTNULL,typeVARCHAR(32)NOTNULL,total_countINTNOTNULL,remain_countINTNOTNULL,threshold_amountDECIMAL(10,2)DEFAULTNULL,discount_amountDECIMAL(10,2)DEFAULTNULL,start_timeDATETIMENOTNULL,end_timeDATETIMENOTNULL,statusVARCHAR(16)NOTNULL);5.2 用户券表
CREATETABLEuser_coupon(idBIGINTPRIMARYKEYAUTO_INCREMENT,user_idBIGINTNOTNULL,template_idBIGINTNOTNULL,statusVARCHAR(16)NOTNULL,order_idBIGINTDEFAULTNULL,received_atDATETIMENOTNULLDEFAULTCURRENT_TIMESTAMP,used_atDATETIMEDEFAULTNULL);5.3 用户券流水表
CREATETABLEuser_coupon_log(idBIGINTPRIMARYKEYAUTO_INCREMENT,user_coupon_idBIGINTNOTNULL,actionVARCHAR(32)NOTNULL,from_statusVARCHAR(16)DEFAULTNULL,to_statusVARCHAR(16)DEFAULTNULL,biz_idBIGINTDEFAULTNULL,created_atDATETIMENOTNULLDEFAULTCURRENT_TIMESTAMP);六、最容易踩的坑
6.1 把券模板和用户券混在一起
后面状态会特别乱。
6.2 没有锁券状态
支付失败和取消场景很容易失控。
6.3 规则计算和发券逻辑耦合太深
后面扩展很痛苦。
6.4 只考虑领券,不考虑回滚和核销
这几乎一定会在上线后出问题。
七、面试中怎么回答
如果面试官问你:
电商优惠券系统一般怎么设计?
你可以这样回答:
第一,优惠券系统我一般会拆成券模板、用户券和券流水三层。模板负责定义规则,用户券负责实例状态,流水负责追踪动作。
第二,用户券状态至少会设计成可用、锁定、已使用、已过期这几类。下单时先锁券,支付成功后核销,支付失败或取消时回滚。
第三,领券和用券这两个过程都需要考虑幂等和并发控制,比如用户重复领券、模板库存超发、支付失败回滚等问题。
第四,如果优惠规则复杂,我会单独抽优惠计算服务,而不是把券计算直接写死在订单服务里。
八、总结
优惠券系统真正难的不是表设计,而是如何把:
- 规则
- 状态
- 并发
- 核销
- 回滚
这些环节串成闭环。
如果只记一句结论,我觉得可以记住这句:
优惠券系统最稳的做法通常不是只做发券,而是“模板、用户券、流水三层拆分 + 锁券核销回滚闭环”。
九、结尾
如果你觉得这篇文章对你有帮助,欢迎点赞、收藏、关注。
后面我会继续整理一些更偏实战的 Java 后端和电商系统设计文章。