优惠券商家后台管理模块
创建优惠券
- 参数校验:使用责任链模式 (
AbstractCouponTemplateChainHandler) 对前端传入的参数进行业务校验(如有效期、库存等)。 - 数据落库:构建
CouponTemplateDO对象并保存到 MySQL 数据库中。 - 缓存预热:为了减轻数据库查询压力,创建成功后立即将模板信息同步到 Redis。
- 使用 Lua 脚本保证
HMSET和EXPIREAT的原子性。 - Redis Key 包含模板ID,值为模板的详情数据。
- 使用 Lua 脚本保证
- 延时关闭:发送 RocketMQ 延时消息。当优惠券有效期结束时,消费者会自动将模板状态更新为“已结束”。
在Redis的存储结构为hash,如下:

并且,RocketMQ的默认延时时间的最大值是3天,如果你创建了3天以上有效期的优惠券,那么会出现错误:
java
org.springframework.messaging.MessagingException: CODE: 13 DESC: timer message illegal, the delay time should not be bigger than the max delay 259200000ms; or if set del msg, the delay time should be bigger than the current time BROKER: 192.168.0.110:10911
For more information, please visit the url, https://rocketmq.apache.org/docs/bestPractice/06FAQ如果确实需要超过 3 天的延时(比如优惠券 7 天后过期提醒),你需要修改 RocketMQ Broker 的配置文件 (broker.conf),调大最大延时上限:
conf
# 开启定时消息功能(5.x 默认通常已开启,但建议确认)
timerWheelEnable = true
# 设置最大延时时间,单位为秒(Seconds)
# 例如设置为 7 天:7 * 24 * 3600 = 604800
# 例如设置为 30 天:30 * 24 * 3600 = 2592000
timerMaxDelaySec = 2592000修改完配置文件后,需要重启 Broker 节点才能生效。
- 内存与磁盘消耗: RocketMQ 的任意秒级定时消息是将消息先存入特殊的 TimerLog 中。延时时间设置得越长、消息量越大,占用的磁盘空间就会越多。同时,时间轮的加载也会占用一定的内存。
- 不要设置得“无限大”: 虽然你可以设置成一年甚至更久,但从架构设计上来说,RocketMQ 不建议作为长期的数据存储组件。如果你的延时需求超过了 30 天(比如一年后的提醒),业界更通用的做法是将任务持久化到数据库(MySQL),通过定时任务(如 XXL-JOB)每天扫描即将到期的任务。
创建分发任务
分发任务分为“立即发送”和“定时发送”两种模式:
- 立即发送:创建后直接发送 MQ 消息,触发异步分发流程。
- 定时发送:
- 创建时仅将任务落库,状态标记为“待发送”。
- 后台定时任务 (XXL-Job) 每隔一段时间扫描数据库,查找
状态=待发送且发送时间 <= 当前时间的任务。 - 扫描到的任务会被投递到 MQ,后续流程与立即发送一致。
Excel 解析与用户信息统计: 上传的 Excel 文件包含待发送的用户名单。为了避免大文件解析阻塞主线程,采用异步线程池进行处理。主流程先保存任务记录,然后提交异步任务去解析 Excel、统计行数(即用户数),并将结果回填到数据库。
并且还有一个兜底策略,使用定时任务补偿统计行数失败的任务,查询 sendNum 为 null 且创建时间超过 5 分钟的记录。
不论是“立即发送”还是“定时发送”的优惠券分发任务,最终都殊途同归:将任务 ID 发送给 RocketMQ。Consumer 接收到消息后,会执行具体的发券逻辑(如调用 User 模块与 Coupon 模块)。 这种设计解耦了“任务触发”与“任务执行”。目前我们实现了一个 Mock Consumer,会在接收到消息后打印日志,便于观察流程流转。
优惠券模板查询
优惠券模板的查询分为详情查询和分页查询。
- 详情查询:根据模板ID和商户ID查询单条记录。
- 分页查询:根据组合条件(名称、类型、状态等)进行分页检索。
增加发行量
商户可以追加优惠券的库存。为了保证数据一致性,采用先更新数据库,再更新缓存的策略。
- 安全性检查:代码中增加了对模板存在性、状态(必须为 ACTIVE)的检查,这有助于防止越权操作或逻辑错误。
- 数据一致性:
Update DB -> Increment Redis。如果 DB 失败,事务回滚,Redis 不更新;如果 DB 成功但 Redis 失败,会导致短暂的不一致(缓存少于 DB),但对于“库存”这种场景,缓存少于 DB 通常不会导致超卖(只会导致少卖,或者下次加载缓存时修复)。为了更严格的一致性,可以考虑引入 Canal 监听 Binlog 或延迟双删,但当前场景下该策略已足够。
结束优惠券模板
商户可以手动提前结束优惠券的发放。
- 状态流转:将模板状态从
ACTIVE(有效) 更新为ENDED(已结束)。 - 操作审计:记录操作日志(
@LogRecord),保留修改前的原始数据,便于后续审计或误操作恢复。 - 同步缓存:DB 更新成功后,同步更新 Redis 中的
status字段,确保前端或其他服务查询缓存时能感知到状态变化。
