TIP
认证服务:注册、登录、密码找回、密码修改、查询我的、查看登录信息等
用户服务:用户详情、完善、修改、足迹、历史、收藏等
会员服务:会员等级、积分变动、新增、消费、充值会员
签到服务:签到、校验、签到记录、抽奖、奖励等
优惠券服务:生成、领取、查询、抵扣等
订单服务:下单、查询、状态等
秒杀服务:下单、秒杀活动、状态等
评价服务:评价、查询、状态等
健身、健康、医疗、教育、社区、社交、电商、旅游、房产等等
通用型:任何项目基本上都可以使用
背景
0.1 优惠劵的目的
优惠券是我们系统中最常见的活跃用户的一种方式,简单的设计就能带来巨大的客流量。通常在活动、促销、甩卖等场景中,我们最常用到的手段无疑是优惠券了
0.2 优惠劵的意义
1、提升用户活跃度
人们总是对 “降价”、“打折” 这样的字眼充满了兴趣,用户也习惯于在了解到商品的价格及优惠力度后再决定购买,所以有优惠的商品才更具有吸引力。
2、增加产品曝光量
用户一券在手,总是让人忍不住翻看可以使用的商品,这无形中增加了平台商品的曝光量。同时好的优惠券会在用户的口口相传中得到推广,这对平台、商家和产品来说,都是一个很好的展现自己口碑的机会。
3、刺激用户的潜在购买需求
当用户的购买行为背后没有充分的购买动机,交易就会轻易的受到其他因素的影响而中断。优惠券的出现满足了用户 “赚到” 的心理,用户就愿意为潜在的购物需求买单。
4、提升用户的购买力
用户的购买力和收入水平成正比,和商品价格呈反比,当价格受到优惠时,用户的购买力也可以得到相应改善。
0.3 优惠劵使用条件的分类
1.体验券
一般针对新品或测试产品向用户免费发放的体验券,意在吸引用户的关注,倾听用户的意见,有时体验券也会以邀请码的形式出现。
2.代金券(又称现金券)
一般使用门槛较低,不会有金额、数量等方面的要求,可以直接使用,若购买商品不够券面金额,通常情况下是不退还差额的,如:新人大礼包、无门槛红包和员工福利等。
3.满减券
通常会有订购数量、订单总价、产品种类等方面的要求,满足条件的订单才可享受满减,如:生活缴费商品满 ¥100 减 ¥2 优惠券。
4.打折券
是直接对商品进行打折,一般商品较贵,购买的用户较少,或者用户订购量大会采用此类型优惠券,如:8.8 折优惠券等。
0.4 优惠劵使用范围分类
1.单品券
为购买单一商品时使用的优惠券
2.系列产品券
为购买某种特定系列产品时所使用的优惠券,用户只需要购买指定系列的产品就可以享用这张优惠券,如:购买无线宝 WiFi5 系列产品优惠券等
3.品类券
为购买某一类商品时使用的优惠券,如:购买清洁类、医药类、生鲜类等优惠券;
4.品牌券
为购买某一品牌商品时使用的优惠券,如:购买华为、京东云等品牌产品所用的优惠券。
0.5优惠劵发放主体分类
1、店铺优惠券
则是店铺自行发放的,如:关注有礼、抽奖、新老顾客回馈等;
2、平台优惠券
是由平台直接发放给用户的优惠券,针对的目标群体范围较广,如:购物津贴、百亿补贴等;
3.政企消费券
成本由政府、企业和平台共同承担,意在提升某些地区消费者的消费能力和消费水平,如:北京消费券等
一、需求
优惠券模板是由运营人员根据一定的条件来设定的,优惠券必须有数量限制并且必须有优惠券码
优惠券模板创建
二、分析
明白需求是什么?
模仿—使用功能
基于优惠券模板实现优惠券的发放,主要实现的是平台券,发放的途径:1.用户抢 2.系统发放
优惠券模板功能:
1.创建优惠券接口,设置各种条件
2.审核优惠券活动接口,根据情况,缓存
3.查询优惠券模板接口
难点:1.系统发放 线程池+分片算法 2.优惠券的缓存
用户优惠券功能:
1.领取接口,实现优惠券领取-超领
2.查询我的优惠券接口
3.查看优惠券抵扣接口
4.抵扣优惠券接口
三、设计
数据库脚本:
CREATE TABLE `t_coupon_template` (
`id` int(11) AUTO_INCREMENT,
`flag` int(11) COMMENT '状态:41.未审核 42.审核通过 43.审核失败',
`name` varchar(64) COMMENT '名字',
`logo` varchar(256) ,
`intro` varchar(256) COMMENT '简介',
`category` int(11) COMMENT '种类: 51-满减;52-折扣;53-立减',
`scope` int(11) COMMENT '使用范围:61-单品;62-商品类型;63-全品',
`scope_id` int(11) COMMENT '对应的id:单品id;商品类型id;全品为0',
`expire_time` datetime COMMENT '优惠券发放结束日期',
`coupon_count` int(11) COMMENT '优惠券发放数量',
`create_time` datetime COMMENT '创建时间',
`user_id` int(11) COMMENT '创建人的ID,后台内部员工',
`user_audit` varchar(100) COMMENT '审核意见',
`template_key` varchar(128) COMMENT '优惠券模板的识别码(有一定的识别度)',
`target` int(11) COMMENT '优惠券作用的人群:71-全体;72-会员等级 73-新用户 74-收费会员',
`target_level` int(11) COMMENT '用户等级要求,默认0',
`send_type` int comment '发放类型:81.用户领取 82.系统发放',
`start_time` datetime comment '优惠券生效日期',
`end_time` datetime comment '优惠券失效日期',
`limitmoney` decimal(10, 2) comment '优惠券可以使用的金额,满减、满折等',
`discount` double comment '减免或折扣' ,
PRIMARY KEY (`id`)
) comment '8.优惠券模板表';
CREATE TABLE `t_usercoupon` (
`id` int(11) AUTO_INCREMENT,
`template_id` int(11) COMMENT '优惠券模板ID',
`user_id` int(11) COMMENT '前端用户ID',
`coupon_code` varchar(70) COMMENT '优惠券码',
`assign_date` datetime COMMENT '优惠券分发时间',
`status` int(11) COMMENT '优惠券状态',
PRIMARY KEY (`id`)
) comment '9.用户优惠券表';
涉及技术栈:
Redis(1.缓存 2.分布式锁 3.倒计时)
RabbitMQ(1.削峰填谷,异步)
线程池(1.批处理,解决系统发放优惠券)
四、实现
基于微服务实现,优惠券服务
4.1 优惠券模板
1.新增优惠券活动接口
2.审核优惠券活动接口
3.查询优惠券活动接口
核心代码:
@Override
public R save(CouponAddDto dto) {
//1.拷贝对象 dto->entity
CouponTemplate template= BeanUtil.copy(dto,dto.getClass().getDeclaredFields(), CouponTemplate.class);
//2.赋值
template.setFlag(CouponAudit.未审核.getCode());
template.setCreateTime(new Date());
//3.操作数据库
if(dao.insert(template)>0){
return RUtil.ok();
}
return RUtil.fail("新增优惠券活动失败!");
}
//审核优惠券活动
@Override
public R audit(CouponAuditDto dto) {
//1.查询数据库
CouponTemplate template=dao.selectById(dto.getId());
//2.验证是否需要审核
if(template!=null && template.getFlag()==CouponAudit.未审核.getCode()){
//3.修改数据库
if(dao.updateFlag(dto)>0){
//审核操作成功
//4.验证优惠券活动是否审核成功
if(dto.getFlag()==CouponAudit.审核通过.getCode()){
//本次优惠券活动 需要执行
//查询优惠券活动,系统发放还是用户领取
//解决方案:1.直接操作 2.策略模式 3.基于MQ-2个队列中
//5.构建MQ发送的消息对象
MqMsgBo msgBo=new MqMsgBo();
msgBo.setId(SnowFlowUtil.getInstance().nextId());
msgBo.setData(template);
String rk="";
//6.验证是哪种发送
if(template.getSendType()== SystemConfig.COUPON_SEND_SYSTEM){
//系统发放-数据批处理,发放条件
msgBo.setType(RabbitMQConstConfig.MQTYPE_COUPONSYS);
rk=RabbitMQConstConfig.RK_COUPONSYS;
}else if(template.getSendType()== SystemConfig.COUPON_SEND_USER){
//用户领取-秒杀-做缓存
msgBo.setType(RabbitMQConstConfig.MQTYPE_COUPONUSE);
rk=RabbitMQConstConfig.RK_COUPONUSE;
}
//7.发送MQ消息
if(StringUtils.hasLength(rk)){
rabbitTemplate.convertAndSend(RabbitMQConstConfig.EX_COUPONTEM,rk,msgBo);
}
return RUtil.ok();
}
}
}
return RUtil.fail("亲,不是合法的操作!");
}
基于MQ异步实现优惠券系统派发:线程池处理业务
@Component
@RabbitListener(queues = RabbitMQConstConfig.Q_COUPONSYS)
public class CouponTemSystemListener {
//实例化日志类对象
private Logger logger= LoggerFactory.getLogger(CouponTemSystemListener.class);
@Resource
private UserProvider provider;//远程服务
@Resource
private UserCouponDao couponDao;
@RabbitHandler
public void handler(MqMsgBo bo){
//1.验证是否为 优惠券活动的系统发放
if(bo.getType()==RabbitMQConstConfig.MQTYPE_COUPONSYS){
//实现系统发放,实现用户优惠券表的数据的批量新增
//2.获取传递的优惠券模板对象
CouponTemplate couponTemplate= (CouponTemplate) bo.getData();
//3.验证是否符合条件:全体成员、等级会员
if(couponTemplate.getTarget()== SystemConfig.COUPON_TARGET_ALL||
couponTemplate.getTarget()==SystemConfig.COUPON_TARGET_LEVEL)
{
//4.查询可以获取优惠券的用户
R r=provider.levels(couponTemplate.getTargetLevel());
//5.验证 远程调用是否成功
if(r.getCode()== RCode.成功.getCode()){
//6.获取uid
List<Integer> uids= (List<Integer>) r.getData();
//实现数据库 用户优惠券表的批量新增-线程池+分片
//7.分片 规则 每个线程任务,只处理1000条数据
int size=uids.size()/SystemConfig.THREAD_COUPON_BATCH;
size=uids.size()%SystemConfig.THREAD_COUPON_BATCH==0?size:size+1;
for (int i=0;i<size;i++){
//8.计算每次的起始数据
final int start=i*SystemConfig.THREAD_COUPON_BATCH;
final int end=(i+1)*SystemConfig.THREAD_COUPON_BATCH;
//9.分配处理任务 给线程池
ThreadPoolSignle.getInstance().poolExecutor.execute(
()->{
//组装用户优惠券对象
List<UserCoupon> coupons=new ArrayList<>();
for(int j=start;j<end;j++){
coupons.add(new UserCoupon(couponTemplate.getId(),uids.get(j), "sys_"+SnowFlowUtil.getInstance().nextId()));
}
//实现批处理
int c=couponDao.insertBatch(coupons);
logger.info("线程池派发优惠券-"+c+"-->"+Thread.currentThread().getName());
});
}
}
}
}
}
}
4.2 优惠券
1.查询我的优惠券
2.领取优惠券
单机锁:synchronized和Lock
synchronized:关键字底层Object 监视器 ,进入的时候验证Object 不存在 加1 ,执行完减1,每次执行完都会校验计数器是否为0,为0,释放锁,在锁上等待的线程会进入就绪状态
用法:3种,锁的范围(粒度)
修饰静态方法—Class对象
修饰实例方法—this
修饰代码块—对象变量
可重入:同一个线程对同一把锁,可以使用多次
公平锁:所有线程,按次序获取锁,排队
非公平锁:抢占、竞争
Lock:接口,实现类:ReentrantLock,底层:AQS(AbstractQueuedSynchronizer)+CAS算法(ABA问题)
//领取优惠券 ?难点-超领
@Override
public R save(int uid,int ul, int ctid) {
//1.验证优惠券是否领取结束
if(RedissonUtils.checkKey(RedisKeyConfig.COUPON_CACHE+ctid)){
//2.校验用户是否领过该优惠券--查询数据库,查询Redis
boolean r=true;
boolean istime=false;
if(RedissonUtils.checkKey(RedisKeyConfig.COUPON_USERS+ctid)){
r=!RedissonUtils.exists(RedisKeyConfig.COUPON_USERS+ctid,uid);
}else {
istime=true;//第一次有人领取这个模板的优惠券
}
if(r){//没有领取过
//3.验证数量是否足够
int count= (int) RedissonUtils.getList(RedisKeyConfig.COUPON_CACHE+ctid,0);
//同步代码块
// synchronized (this) {
// if (count > 0) {
// //4.校验领取是否有用户等级的限制
// int level = (int) RedissonUtils.getList(RedisKeyConfig.COUPON_CACHE + ctid, 1);
// if (level > 0) {
// //改优惠券的领取,要求用户等级,关于等级:1.前端直接传递 2.远程服务调用
// //5.查询用户等级,校验是否满足规则
// if (ul < level) {
// return RUtil.fail("亲,你不满足领取的资格!");
// }
// }
//
// //6.生成用户优惠券信息
// UserCoupon coupon = new UserCoupon(ctid, uid, "user_" + SnowFlowUtil.getInstance().nextId());
// //7.新增优惠券到数据库
// if (dao.insert(coupon) > 0) {
//
// //更改数量
// RedissonUtils.setList(RedisKeyConfig.COUPON_CACHE + ctid, 1, count - 1);
// //记录当前用户已经领取
// RedissonUtils.setSet(RedisKeyConfig.COUPON_USERS + ctid, uid + "");
// //设置 有效期,优惠券模板的剩余时间
// if (istime) {
// RedissonUtils.expire(RedisKeyConfig.COUPON_USERS + ctid, RedissonUtils.ttl(RedisKeyConfig.COUPON_CACHE + ctid));
// }
//
// //返回结果
// return RUtil.ok();
// } else {
// return RUtil.fail("系统故障,领取失败!");
// }
// } else {
// return RUtil.fail("亲,优惠券已被领完!");
// }
// }
//Lock JUC下面 性能
Lock lock=new ReentrantLock();
lock.lock();//开始锁,加锁
try{
if (count > 0) {
//4.校验领取是否有用户等级的限制
int level = (int) RedissonUtils.getList(RedisKeyConfig.COUPON_CACHE + ctid, 1);
if (level > 0) {
//改优惠券的领取,要求用户等级,关于等级:1.前端直接传递 2.远程服务调用
//5.查询用户等级,校验是否满足规则
if (ul < level) {
return RUtil.fail("亲,你不满足领取的资格!");
}
}
//6.生成用户优惠券信息
UserCoupon coupon = new UserCoupon(ctid, uid, "user_" + SnowFlowUtil.getInstance().nextId());
//7.新增优惠券到数据库
if (dao.insert(coupon) > 0) {
//更改数量
RedissonUtils.setList(RedisKeyConfig.COUPON_CACHE + ctid, 1, count - 1);
//记录当前用户已经领取
RedissonUtils.setSet(RedisKeyConfig.COUPON_USERS + ctid, uid + "");
//设置 有效期,优惠券模板的剩余时间
if (istime) {
RedissonUtils.expire(RedisKeyConfig.COUPON_USERS + ctid, RedissonUtils.ttl(RedisKeyConfig.COUPON_CACHE + ctid));
}
//返回结果
return RUtil.ok();
} else {
return RUtil.fail("系统故障,领取失败!");
}
} else {
return RUtil.fail("亲,优惠券已被领完!");
}
}finally {
lock.unlock();//释放锁
}
}else {
//领取过
return RUtil.fail("亲,你已经领取过了!");
}
}else {
return RUtil.fail("亲,活动已结束!");
}
}
分布式锁:解决集群下,同一资源排队访问
如果我们的项目是集群部署(多态服务器)下,防止线程安全,可以使用分布式锁
推荐使用:Redssion的RedLock
RedLock:Redis Distributed Lock;即使用redis实现的分布式锁
算法实现了多redis实例的情况,相对于单redis节点来说,优点在于防止了最低保证分布式锁的有效性及安全性的要求如下:
1.互斥;任何时刻只能有一个client获取锁
2.释放死锁;即使锁定资源的服务崩溃或者分区,仍然能释放锁
3.容错性;只要多数redis节点(一半以上)在使用,client就可以获取和释放锁单节点故障造成整个服务停止运行的情况;并且在多节点中锁的设计,及多节点同时崩溃等各种意外情况有自己独特的设计方法
@Override
public R saveRedlock(int uid, int ul, int ctid) {
//1.验证优惠券是否领取结束
if(RedissonUtils.checkKey(RedisKeyConfig.COUPON_CACHE+ctid)){
//2.校验用户是否领过该优惠券--查询数据库,查询Redis
boolean r=true;
boolean istime=false;
if(RedissonUtils.checkKey(RedisKeyConfig.COUPON_USERS+ctid)){
r=!RedissonUtils.exists(RedisKeyConfig.COUPON_USERS+ctid,uid);
}else {
istime=true;//第一次有人领取这个模板的优惠券
}
if(r){//没有领取过
//3.验证数量是否足够
int count= (int) RedissonUtils.getList(RedisKeyConfig.COUPON_CACHE+ctid,0);
//获取分布式锁的对象
RLock rLock=RedissonUtils.getLock(RedisKeyConfig.COUPON_LOCK+ctid);
try {
//尝试添加分布式锁
if(rLock.tryLock(RedisKeyConfig.COUPON_LOCK_TIME, TimeUnit.SECONDS)){
//成功,就继续
if (count > 0) {
//4.校验领取是否有用户等级的限制
int level = (int) RedissonUtils.getList(RedisKeyConfig.COUPON_CACHE + ctid, 1);
if (level > 0) {
//改优惠券的领取,要求用户等级,关于等级:1.前端直接传递 2.远程服务调用
//5.查询用户等级,校验是否满足规则
if (ul < level) {
return RUtil.fail("亲,你不满足领取的资格!");
}
}
//6.生成用户优惠券信息
UserCoupon coupon = new UserCoupon(ctid, uid, "user_" + SnowFlowUtil.getInstance().nextId());
//7.新增优惠券到数据库
if (dao.insert(coupon) > 0) {
//更改数量
RedissonUtils.setList(RedisKeyConfig.COUPON_CACHE + ctid, 1, count - 1);
//记录当前用户已经领取
RedissonUtils.setSet(RedisKeyConfig.COUPON_USERS + ctid, uid + "");
//设置 有效期,优惠券模板的剩余时间
if (istime) {
RedissonUtils.expire(RedisKeyConfig.COUPON_USERS + ctid, RedissonUtils.ttl(RedisKeyConfig.COUPON_CACHE + ctid));
}
//返回结果
rLock.unlock();
return RUtil.ok();
} else {
rLock.unlock();
return RUtil.fail("系统故障,领取失败!");
}
} else {
rLock.unlock();
return RUtil.fail("亲,优惠券已被领完!");
}
}else {
//失败,直接返回 领取失败
return RUtil.fail("亲,没有了!");
}
} catch (InterruptedException e) {
e.printStackTrace();
return RUtil.fail("亲,系统故障!");
}
}else {
//领取过
return RUtil.fail("亲,你已经领取过了!");
}
}else {
return RUtil.fail("亲,活动已结束!");
}
}
五、测试
测试用例:
新增优惠券模板:
{
"category": 52,
"couponCount": 1000,
"discount": 50,
"endTime": "2022-05-19 23:59:59",
"expireTime": "2022-05-17 23:59:59",
"intro": "测试系统发送优惠券",
"limitmoney": 100,
"logo": "",
"name": "测试方便面满100减50元",
"scope": 63,
"scopeId": 0,
"sendType": 82,
"startTime": "2022-05-18 00:00:00",
"target": 71,
"targetLevel": 0
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
{
"category": 53,
"couponCount": 10,
"discount": 50,
"endTime": "2022-05-20T10:08:55.000+08:00",
"expireTime": "2022-05-18T10:08:55.000+08:00",
"intro": "测试系统发送优惠券",
"limitmoney": 100,
"logo": "",
"name": "水饺立减50元",
"scope": 63,
"scopeId": 0,
"sendType": 81,
"startTime": "2022-05-18T10:08:55.000+08:00",
"target": 71,
"targetLevel": 0
}