如何设计一个优惠券系统

article/2025/10/29 0:44:46

背景

部门为一个租房房源平台,为各个商家提供房源发布&C端曝光获客的功能,现在要构建一个优惠券系统,用于各个节假日节点进行商家营销活动。形式主要以商家在B端参与活动,对房源绑定优惠券,将租赁价格进行优惠,来在C端吸引用户进行租房。

1. 业务梳理

在清楚了大致的业务背景后,下面来进行整体的业务流程梳理,大致如下图所示。

首先,平台建立好活动,在商家B端将可报名的活动展示出来,商家通过报名对应优惠力度的活动,来建立对应优惠的优惠券。

然后,通过将房源与对应的优惠券建立绑定关系,来对房源数据打上优惠券标识。这样一来在C端展示房源时,就可以进行对应的优惠房源筛选,以及让用户在房源上进行领取某个类别的优惠券。

最后,用户在C端领取优惠券后,可联系商家进行实地房源考察,如果双方达成协议,即可在线上签约。而在签约时即可使用对应优惠券,实现相应的价格优惠。

至此,就是整个系统的完整正向流程了。

2. 技术设计

下面来对每个环节的进行相应的技术设计。

2.1 建立活动

2.1.1 数据表

活动信息需要如下数据项

  • 报名时间范围

  • 活动时间范围

  • 优惠类型和具体的优惠力度

  • 可报名的城市

下面就是活动信息数据表的具体设计

CREATE TABLE `t_activity` (`activeId` bigint(20) NOT NULL COMMENT '活动ID',`title` varchar(256) NOT NULL COMMENT '活动名称',`applyStartTime` timestamp NULL DEFAULT NULL COMMENT '报名开始时间',`applyEndTime` timestamp NULL DEFAULT NULL COMMENT '报名停止时间',`activityStartTime` timestamp NULL DEFAULT NULL COMMENT '活动开始时间',`activityEndTime` timestamp NULL DEFAULT NULL COMMENT '活动结束时间',`cityIds` varchar(256) NOT NULL COMMENT '覆盖城市,多个逗号分隔',`couponType` tinyint(4) NOT NULL DEFAULT '0' COMMENT '优惠类型,1 直减;2 折扣;3免费住N天;4免押金;5特价房',`lowerLimit` int NOT NULL DEFAULT 0 COMMENT '优惠数值下限',`upperLimit` int NOT NULL DEFAULT 0 COMMENT '优惠数值上限',`description` text COMMENT '活动描述',`cubeType` smallint(6) NOT NULL DEFAULT '1001' COMMENT '活动类型',`foreignId` bigint(20) NOT NULL DEFAULT '0' COMMENT '外部ID',`status` tinyint(4) NOT NULL DEFAULT '0' COMMENT '活动状态',`createTime` timestamp NULL DEFAULT NULL COMMENT '创建时间',`updateTime` timestamp NULL DEFAULT NULL COMMENT '更新时间',`recordStatus` tinyint(4) NOT NULL DEFAULT '0' COMMENT '数据状态',PRIMARY KEY (`activeId`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COMMENT='活动信息表';

2.1.2 数据读取

在商家端可以通过status字段和cityIds字段来进行可报名活动的展示。

C端读取的展示代码里可以使用设计模式中的代理模式来加一层缓存,在进行中的活动会将数据推入到cache层中。

2.1.3 活动状态流转

通过crontab定时任务,每分钟进行时间段的检查,来更新对应的status字段,完成活动状态的流转。

2.1.3 数据读取

可以使用设计模式中的代理模式来加一层缓存,非强实时的查询走cache,cache中不存在或需实时数据的再走db。

2.2 商家报名建券

2.2.1 数据表

CREATE TABLE `t_couponmeta` (`couponMetaId` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '券id',`appId` int(11) NOT NULL DEFAULT '1' COMMENT '区分建立来源',`activeId` bigint(20) NOT NULL DEFAULT '0' COMMENT '活动ID',`companyId` bigint(20) NOT NULL COMMENT '公司编号',`cityId` int(11) NOT NULL COMMENT '城市id',`companyName` varchar(255) DEFAULT NULL COMMENT '公司名称',`companyShortName` varchar(255) DEFAULT NULL COMMENT '公司简称',`couponType` tinyint(4) NOT NULL COMMENT '优惠券类型',`title` varchar(256) NOT NULL COMMENT '优惠券名称',`directDiscount` int(11) NOT NULL DEFAULT '0' COMMENT '直减券优惠力度',`discount` int(11) NOT NULL DEFAULT '0' COMMENT '折扣力度',`freeLive` int(11) NOT NULL DEFAULT '0' COMMENT '免费住n天券',`threshold` varchar(256) NOT NULL COMMENT '使用门槛',`deduction` tinyint(4) NOT NULL DEFAULT '1' COMMENT '抵扣说明 1首月抵扣,2 平摊到月',`totalAmount` int(11) NOT NULL DEFAULT '0' COMMENT '券总数',`applyAmount` int(11) NOT NULL DEFAULT '0' COMMENT '已领取总数',`activityStartTime` timestamp NULL DEFAULT NULL COMMENT '活动开始时间',`activityEndTime` timestamp NULL DEFAULT NULL COMMENT '活动结束时间',`startTime` timestamp NULL DEFAULT NULL COMMENT '券使用开始时间',`expireTime` timestamp NULL DEFAULT NULL COMMENT '券使用结束时间',`status` int(11) NOT NULL DEFAULT '10' COMMENT '10:新建未启用,20:已启用,30:过期, 40 已结束 50 已中止',`expireType` tinyint(4) NOT NULL DEFAULT '1' COMMENT '类型:1固定有效期类型,2浮动有效期类型',`validPeriod` tinyint(4) NOT NULL DEFAULT '0' COMMENT '浮动有效期(单位:天)',`tenantRange` tinyint(1) NOT NULL DEFAULT '1' COMMENT '租客范围枚举值',`customScope` varchar(256) NOT NULL DEFAULT '' COMMENT '自定义租客范围',`comment` varchar(50) DEFAULT NULL COMMENT '备注',`cubeType` smallint(6) NOT NULL DEFAULT '1001' COMMENT '活动类型',`updateTime` timestamp NULL DEFAULT NULL COMMENT '更新时间',`createTime` timestamp NULL DEFAULT NULL COMMENT '创建时间',`recordStatus` tinyint(4) DEFAULT '0' COMMENT '状态 0默认 -1删除',PRIMARY KEY (`couponMetaId`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='优惠券表';

2.2.2 数据读取

同样可以使用设计模式中的代理模式来加一层缓存,对于C端大量读取优惠券信息的场景,需要让读请求尽量都在cache层处理完成,降低db的压力。

2.2.3 状态流转

优惠券的状态流转如图所示

优惠券状态

大部分状态取决于活动的状态(除中止和过期),所以在活动状态的crontab任务中,当发现活动状态发生变更后,会将下面的优惠券状态变更任务以MQ发送出来,异步来修改优惠券的状态。

而优惠券状态的变更又会涉及联动数据的更新,举例来说:

  • 状态从开始->启用时,需要更新房源C端索引数据的优惠券标识,并初始化库存数据(这部分在领券时候细说);

  • 状态从启用->结束时,需要将房源上的优惠券标识去掉,清空缓存数据;

  • ...

可以想到,在每个状态流转的时候,都有很多操作来做。为了保证业务逻辑清晰,状态流转我采用了状态模式来进行实现,类图如下所示:

而状态更新后,C端房源索引数据/b端基础房源数据/缓存中的数据,这些数据的更新使用了观察者模式监听状态的变更,在观察者中通过发送mq异步来进行数据更新,类图如下所示:

 通过将需要的观察者注册到CouponStateMachine中,在进行实际的doChangeStatus操作后,notify所有的观察者即可保证联动数据的正确性。

2.3 绑定优惠券

2.3.1 数据表

将商家b端绑定了的优惠券全量(新建/启用中/未过期)数据存于MySQL数据表中,表结构比较简单,如下所示:

CREATE TABLE `t_bindcoupon` (`id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT 'id',`couponMetaId` int(11) NOT NULL COMMENT '券id',`companyId` bigint(20) NOT NULL COMMENT '公司编号',`activityStatus` tinyint(4) NOT NULL COMMENT '状态 0 准备中 1 活动中 2 活动结束 券未失效 3活动结束券失效',`houseId` bigint(20) NOT NULL DEFAULT '0' COMMENT '房源id',`recordStatus` tinyint(4) NOT NULL COMMENT '数据状态 0 有效,-1 失效',`createTime` timestamp NULL DEFAULT NULL COMMENT '创建时间',`updateTime` timestamp NULL DEFAULT NULL COMMENT '更新时间',PRIMARY KEY (`id`),KEY `idx_companyId_couponMetaId` (`companyId`,`couponMetaId`),KEY `idx_planeId` (`planeId`),KEY `idx_houseid_activitystatus` (`houseId`,`activityStatus`)
) ENGINE=InnoDB AUTO_INCREMENT=17379 DEFAULT CHARSET=utf8 COMMENT='优惠券绑定范围表'

该表主要是记录房源和优惠券之间的绑定关系,并记录当前绑定的状态,该状态用于限制同一个房源不可以绑太多的有效优惠券,限制商家为了增加c端曝光,胡乱操作。

操作房源绑定优惠券的时候,会使用分布式锁,避免并发绑定操作导致超限

2.3.2 数据展示

数据的展示依赖于C端的索引数据,索引数据中存储的都是在生效状态的优惠券信息,字段结合业务场景用于筛选和展示,具体如下:

  1. 优惠券ID(多值)

  2. 活动ID(多值)

  3. 参加的活动类型(多值)

通过索引筛选条件检索出房源数据后,即可获取房源的完整数据来获取该房源绑定了哪些优惠券,进行优惠券数据的展示。

2.3.3 状态流转

对于绑定关系的状态实时更新有两个触发动作,根据情况去更新数据的status和recordStatus两个字段:

  1. 依托于2.2.3中的优惠券状态的MQ消息

  2. db绑定数据发生变化时(改/增/减),同样也会发送MQ,异步更新索引中的数据。

2.4 C端用户领券

C端用户领券是一个比较重要的地方,核心要求就是绝对不能多领,尽可能避免少领。

具体设计流程图如下:

 

流程大概分为三步:

  1. 请求校验

  2. redis库存扣减

  3. 领取记录和更新库存任务以事务形式写入MySQL

下面根据图中所示逐步进行说明

2.4.1 数据读取

由于C端浏览券详情&领券可能会是一个并发量较高的操作,所以尽可能都从缓存中读数据,包括以下数据:

  1. 活动信息

  2. 优惠券信息

  3. 优惠券库存

当活动开始前五分钟会禁止编辑活动信息和优惠券信息,活动开始时将上述数据推入缓存中,不设置过期时间,待活动状态转为结束时再清理数据。

同时在web服务集群中,对这1 2数据项做了一层短时间的本地缓存,减少请求redis集群的网络开销。

2.4.2 校验

首先,服务端收到用户领券的请求后,会在redis中校验是否存在领取记录cache。这里可以使用布隆过滤器来实现,key为优惠券id,若用户id存在其中则直接返回。

然后,会进行优惠券状态校验,优惠券数据在活动开始时即推入了redis中,所以直接用redis中的数据校验即可。

最后,会校验是否存在该优惠券的存库数据,为了提高容错性和可用性,若不存在则发送一个初始化库存任务的MQ,然后直接返回,由MQ异步来重新初始化库存数据,避免缓存击穿的问题。

2.4.3 库存扣减

直接操作db,以使用乐观锁形式扣减库存,MySQL的库存更新操作会成并发热点,请求都会在行锁的争抢中阻塞,支持的并发量有限,并且会给db带来压力。

由于redis可以保证操作的原子性,并且数据在内存中,适合高并发场景,所以通过redis来完成库存的扣减。

将db的库存更新操作通过db消息任务表,进行异步化,串行化,避免阻塞的同时也降低锁的争抢。

但是扣减库存以redis为准的话,就分为几种情况:

  1. redis扣减成功,但是db领取记录和更新库存任务写入失败,执行回滚,incr库存数量。

  2. redis扣减失败(没库存/redis宕机),不会执行db操作。

  3. redis扣减成功,db事务执行时,服务运行机器重启或宕机,没有回滚库存,产生少领情况

  4. redis扣减成功后redis主库宕机,DB写入数据成功,但扣减数据未同步到从库,使用从库进行扣减时产生超领现象

1 2 属于正常情况,3 4 属于异常情况。

对于情况3, 可以在redis中库存扣减光时,触发异步任务来对比库存数据,若还有可领取库存,则更新redis的库存信息,达到避免少领的情况。

对于情况4,由于redis无法保证主从强一致,在数据操作丢失的情况下,就有可能会产生超领情况。

我的想法是,有以下几种方式:

  1. 若更新库存操作已经存在超领的情况,将用户领取优惠券的数据进行删除或冻结,避免带来损失。

  2. 监控剩余库存量每变动5%,就执行异步任务将优惠券状态进行冻结,让C端无法领券。然后将当前消息事务表中db的扣减任务都处理完成后,进行redis和db的数据校验同步,同步完成后将优惠券解冻,恢复正常领取。

  3. 由于redis属于AP,若要保证数据的强一致性则牺牲可用性,改为使用CP的存储。

  4. 优惠券核销时检查核销数量是否超过总数量,若达到阈值,则提示优惠券不可用。

  5. 根据优惠券发放总数量,生成一批优惠券id,同时存入db与redis队列中,通过pop id生成优惠券领取记录,根据id是否相同来限制重复领取,从而达到防止超领的情况。

第一种方式,用户领券成功到更新db库存任务的执行,在这中间时间窗口很小的情况下,可以尽可能的避免超领并使用的情况。

第二种方式,也不能完全解决超量的问题,只能以牺牲很少时间的领取功能,矫正领取请求非激增的情况下数据不一致的情况。

第三种方式不做赘述。

第四种方式,被动的进行数量的校验,保证使用的优惠券不会超过发放的总数量,个人认为是比较柔和的影响范围最小的处理方式了,只不过可能会引起客诉,(为什么领了还是不能用!!!)

第五种方式,db中根据优惠券发放总数量生成一批领取记录,但是领取用户id以及领取时间空着。将这批记录的id存入redis队列中,扣库存的时候pop该队列,获取id以后,通过乐观锁方式更新对应id的数据行(where id = x and userId = 0),若更新失败则领取失败。这样可以有效的防止第四种情况产生,需要考虑的是若发放数量过多,而实际领取很少,还需活动结束后清理占用的数据表空间。事前需要预热插入db中的数据,事后需要清理没有绑定userId的数据。

在我的业务场景下,考量过后主要选用了第四种方式,第二种方式选择了每天凌晨四点进行一次,第一种没有删除而仅仅是进行了报警。

若有更好的方式,希望大神能够指点一下

2.5 优惠券核销

以微服务形式,提供用户优惠券获取接口,以及状态变更接口给调用方使用,状态流转分为未使用、锁定、已使用三种状态。

当客户下了签约订单时,将对应使用的优惠券进行锁定。若订单完成则调用接口将状态流转为已使用,若订单取消则回滚优惠券的状态为未使用。

未来可优化方向

  1. 用户的优惠券数据分库分表,进行数据水平拆分,提升db读写能力。

  2. redis集群以分片形式部署,提升可用性及容量。

  3. 集群拆分,每个集群仅处理部分优惠券请求,通过网关打散请求到不同的pod中。

  4. 引入jd-hotkey组件,热key实时同步到集群本地缓存中,减少访问分布式缓存。

  5. 引入canal组件,通过binlog同步db更新的信息,更新缓存并进行数据联动更新。

总结

至此,一个完整的优惠券系统就构建完毕了。

遵循读多写少用缓存,写多读少用队列的原则。

对于展现的活动数据,代码通过代理模式尽可能的通过缓存进行读取。使用了多级缓存的同时,为了避免产生缓存击穿的场景,对于活动中的数据都采用了主动推数据到redis中的方式。

对于活动->优惠券->房源的联动数据的写操作,代码通过状态模式+观察者模式实现,以及MQ控制并发量的异步更新。

对于库存扣减采用了redis,依托于它的原子性,但是redis不保证集群内部数据强一致性。为了避免超领带来的损失问题,在核销优惠券时进行了数量阈值校验。

对于热点的db库存更新则采用了db事务消息表,通过事务保证领取记录插入成功的同时一定会落入更新库存任务,从而异步串行的进行库存更新。

 


http://chatgpt.dhexx.cn/article/C3s4hL9Q.shtml

相关文章

大厂的优惠券系统是如何设计的?

1 Scenario 场景 电商系统的促销手段(Electronic Commerce Systems): 优惠券拼团砍价老带新 优惠券的种类 满减券直减券折扣券 优惠券系统的核心流程 发券 **发券的方式:**同步发送 or 异步发送 领券 谁能领? …

优惠券系统架构设计与实践

技术琐话 2022-01-20 12:03 以下文章来源于vivo互联网技术 ,作者Yan Chao vivo互联网技术. 分享 vivo 互联网技术干货与沙龙活动,推荐最新行业动态与热门会议。 一、业务背景 优惠券是电商常见的营销手段,具有灵活的特点,既可以…

python3.5 将py文件转换成EXE文件

方法其实就是利用pyinstaller和pywin32 1.安装pyinstaller 输入cmd进入windows命令提示符,如果没有设置python环境变量,则转到python安装目录的scripts目录下, 如果已经设置python环境变量,可以跳过上述步骤。然后输入 pip inst…

把python文件转exe文件 和 打包报错处理

今天,我教大家怎么把py文件转换成exe文件, 首先,我们要用到一个第三方库,他就是pyinstaller pip install pyinstaller zip包下载(CSDN)免费 Pyinstaller代码压缩包-Python文档类资源-CSDN下载 此外&am…

python封装成exe

最近写了一个小小的程序,需要进行封装exe,为了简单,就直接用了pyinstaller这个模块,对于python3.6版本的童鞋来说,简直方便的不要。下面就给大家介绍一下如何用pyinstaller去封装程序为exe程序。 首先,需要…

python如何封装成exe

python文件封装成exe 第一种:.py文件直接封装成exe第二种:整个项目封装成exe ) 第一种:.py文件直接封装成exe 1.cmd进入py文件所在的目录 备注:在py文件所在的目录下,按住shift鼠标右击,然后找到“在此处…

Python打包成exe文件_详细操作

Python打包成exe文件 前言一、安装pyinstaller1.1 安装pyinstaller,使用安装命令:1.2 如果遇到需要更新版本请输入:1.3 检查是否正确安装1.4 稍等,水一下; 二、python打包成exe文件(附带图标打包)2.1 第一种…

将Python文件转化为exe文件

第一步:首先安装相关工具 在Windos中打开命令行窗口 输入pip install pyinstaller 出现以下情况表示安装成功 第二步:打包成exe文件 若需将xxx.py文件打包,只需在终端执行: 注:终端需切换至xxx.py文件所在目录下。…

如何将python的.py文件转换为可执行的.exe文件。

首先,我写了一个print(“hello,world”).py文件。命名为hello.py保存在我的电脑C盘的C:\Users\ly目录下如图所示。 ps:尽量选择在这个文件夹下,如果选择其他盘的文件夹下,生成的.exe的dist文件夹也会出现在…

将.py文件转化为.exe文件

将.py文件转化为.exe文件首先需要第三方库 pyinstaller 1.如果没有安装pyinstaller,则在命令提示符输入 pip installer pyinstaller 如果已安装,则会显示 表示这几个都安装好了。 2.下载安装pyinstaler运行时所需windows扩展:pywin32 mham…

[272]如何把Python脚本导出为exe程序

文章目录 一.pyinstaller简介二.pyinstaller在windows下的安装三.打包四.小实例(windows下)附加 一.pyinstaller简介 pyinstaller将Python脚本打包成可执行程序,使在没有Python环境的机器上运行 最新版是pyinstaller 3.1.1。支持python2.7和…

把py转化成exe(干货)

问题提出: 无奈别人的电脑没有装python编译环境,无法运行py文件。   本教程为在window环境下,使用PyInstaller3.4将Python脚本打包成exe可执行程序。 使用命令: pip install pyinstaller   上图为成功安装效果图&#xff0…

简单3步将你的python转成exe格式

使用pyinstaller可以将你的.py文件直接转换成.exe格式,在没有部署python环境的windows机器上直接运行你的程序!! 废话不多说,直接上正文。 1. 在命令行用pip安装 pyinstaller包 pip install pyinstaller 2.下载安装pyinstaler…

.py文件转为.exe文件的方法

写完一个Python程序想给朋友看?朋友电脑里没Python运行不了? 别急,这篇文章来帮你。 把你的python文件转成exe文件,打开方便,瞬间提示一个档次。 (我个人也遇到过这样的问题,看了别人的博客解…

将Python程序打包成exe文件

我新写了一篇更加完整的文章,与这篇文章相比,它新增了两种打包方式:多python文件打包和含有资源文件的打包方式,具体请戳链接: 用 Pyinstaller 模块将 Python 程序打包成 exe 文件(全网最全面最详细&#x…

Python 将.py转换为.exe详解

本文是由熊猫大哥大的博客修改而来(所以就不能说是“原创”了) 原博客链接:Python学习笔记(15)-Python代码转换为exe可执行程序详解(下面会提到本文与其的一点不同) 建议看看原博客,再看此文,…

Python学习笔记(15)-Python代码转换为exe可执行程序详解

点此查看 零基础Python全栈文章目录及源码下载 本文目录 1. 简介2. pyInstaller安装配置2.1 下载PyInstaller2.2 下载后解压PyInstaller2.3 下载pywin322.4 安装pywin322.5 安装PyInstaller 3. 制作exe3.1 编写python文件3.2 将代码文件放至pyinstaller目录3.3 制作exe3.4 查看…

如何将py文件转化为exe

如何将py文件转化为exe 第一步:安装pyinstaller库。 首先,winr打开运行窗口,输入 cmd,在cmd窗口中输入python -m pip install pyinstaller 等待计算机安装,完成后会有successfully等字眼 第二步:打包文件…

python文件转换成exe可执行的windows文件

一、介绍 python的程序需要运行环境有时候很不方便,因为要交给别人代码可能因为环境的原因运行各种问题,这里给出直接讲python程序转换成exe文件,很方便直接给执行程序就可以,也不用担心运行环境问题而运行不了 二、工具 1、安装…

将Python文件编译为exe可执行程序

Python程序py格式文件的优点是可以跨平台,但运行必须有Python环境,没有Python环境无法运行py格式文件。有没有方法,用户不同安装Python就可直接运行开发的项目工程?答案是肯定的。这就涉及到需要将Python的.py格式文件编写的脚本编…