TIP

认证服务:注册、登录、密码找回、密码修改、查询我的、查看登录信息等

用户服务:用户详情、完善、修改、足迹、历史、收藏等

会员服务:会员等级、积分变动、新增、消费、充值会员

签到服务:签到、校验、签到记录、抽奖、奖励等

优惠券服务:生成、领取、查询、抵扣等

订单服务:下单、查询、状态等

秒杀服务:下单、秒杀活动、状态等

评价服务:评价、查询、状态等

健身、健康、医疗、教育、社区、社交、电商、旅游、房产等等

通用型:任何项目基本上都可以使用

背景

0.1 优惠劵的目的

优惠券是我们系统中最常见的活跃用户的一种方式,简单的设计就能带来巨大的客流量。通常在活动、促销、甩卖等场景中,我们最常用到的手段无疑是优惠券了

0.2 优惠劵的意义

优惠券服务 - 图1

1、提升用户活跃度

人们总是对 “降价”、“打折” 这样的字眼充满了兴趣,用户也习惯于在了解到商品的价格及优惠力度后再决定购买,所以有优惠的商品才更具有吸引力。

2、增加产品曝光量

用户一券在手,总是让人忍不住翻看可以使用的商品,这无形中增加了平台商品的曝光量。同时好的优惠券会在用户的口口相传中得到推广,这对平台、商家和产品来说,都是一个很好的展现自己口碑的机会。

3、刺激用户的潜在购买需求

当用户的购买行为背后没有充分的购买动机,交易就会轻易的受到其他因素的影响而中断。优惠券的出现满足了用户 “赚到” 的心理,用户就愿意为潜在的购物需求买单。

4、提升用户的购买力

用户的购买力和收入水平成正比,和商品价格呈反比,当价格受到优惠时,用户的购买力也可以得到相应改善。

0.3 优惠劵使用条件的分类

1.体验券

优惠券服务 - 图2

一般针对新品或测试产品向用户免费发放的体验券,意在吸引用户的关注,倾听用户的意见,有时体验券也会以邀请码的形式出现。

2.代金券(又称现金券)

优惠券服务 - 图3

一般使用门槛较低,不会有金额、数量等方面的要求,可以直接使用,若购买商品不够券面金额,通常情况下是不退还差额的,如:新人大礼包、无门槛红包和员工福利等。

3.满减券

优惠券服务 - 图4

通常会有订购数量、订单总价、产品种类等方面的要求,满足条件的订单才可享受满减,如:生活缴费商品满 ¥100 减 ¥2 优惠券。

4.打折券

优惠券服务 - 图5

优惠券服务 - 图6

是直接对商品进行打折,一般商品较贵,购买的用户较少,或者用户订购量大会采用此类型优惠券,如:8.8 折优惠券等。

0.4 优惠劵使用范围分类

1.单品券

优惠券服务 - 图7

为购买单一商品时使用的优惠券

2.系列产品券

优惠券服务 - 图8

为购买某种特定系列产品时所使用的优惠券,用户只需要购买指定系列的产品就可以享用这张优惠券,如:购买无线宝 WiFi5 系列产品优惠券等

3.品类券

优惠券服务 - 图9

为购买某一类商品时使用的优惠券,如:购买清洁类、医药类、生鲜类等优惠券;

4.品牌券

优惠券服务 - 图10

为购买某一品牌商品时使用的优惠券,如:购买华为、京东云等品牌产品所用的优惠券。

0.5优惠劵发放主体分类

1、店铺优惠券

优惠券服务 - 图11

则是店铺自行发放的,如:关注有礼、抽奖、新老顾客回馈等;

2、平台优惠券

优惠券服务 - 图12

是由平台直接发放给用户的优惠券,针对的目标群体范围较广,如:购物津贴、百亿补贴等;

3.政企消费券

优惠券服务 - 图13

成本由政府、企业和平台共同承担,意在提升某些地区消费者的消费能力和消费水平,如:北京消费券等

一、需求

优惠券模板是由运营人员根据一定的条件来设定的,优惠券必须有数量限制并且必须有优惠券码

优惠券服务 - 图14

优惠券模板创建

优惠券服务 - 图15

二、分析

明白需求是什么?

模仿—使用功能

基于优惠券模板实现优惠券的发放,主要实现的是平台券,发放的途径:1.用户抢 2.系统发放

优惠券模板功能:

1.创建优惠券接口,设置各种条件

2.审核优惠券活动接口,根据情况,缓存

3.查询优惠券模板接口

难点:1.系统发放 线程池+分片算法 2.优惠券的缓存

用户优惠券功能:

1.领取接口,实现优惠券领取-超领

2.查询我的优惠券接口

3.查看优惠券抵扣接口

4.抵扣优惠券接口

三、设计

数据库脚本:

  1. CREATE TABLE `t_coupon_template` (
  2. `id` int(11) AUTO_INCREMENT,
  3. `flag` int(11) COMMENT '状态:41.未审核 42.审核通过 43.审核失败',
  4. `name` varchar(64) COMMENT '名字',
  5. `logo` varchar(256) ,
  6. `intro` varchar(256) COMMENT '简介',
  7. `category` int(11) COMMENT '种类: 51-满减;52-折扣;53-立减',
  8. `scope` int(11) COMMENT '使用范围:61-单品;62-商品类型;63-全品',
  9. `scope_id` int(11) COMMENT '对应的id:单品id;商品类型id;全品为0',
  10. `expire_time` datetime COMMENT '优惠券发放结束日期',
  11. `coupon_count` int(11) COMMENT '优惠券发放数量',
  12. `create_time` datetime COMMENT '创建时间',
  13. `user_id` int(11) COMMENT '创建人的ID,后台内部员工',
  14. `user_audit` varchar(100) COMMENT '审核意见',
  15. `template_key` varchar(128) COMMENT '优惠券模板的识别码(有一定的识别度)',
  16. `target` int(11) COMMENT '优惠券作用的人群:71-全体;72-会员等级 73-新用户 74-收费会员',
  17. `target_level` int(11) COMMENT '用户等级要求,默认0',
  18. `send_type` int comment '发放类型:81.用户领取 82.系统发放',
  19. `start_time` datetime comment '优惠券生效日期',
  20. `end_time` datetime comment '优惠券失效日期',
  21. `limitmoney` decimal(10, 2) comment '优惠券可以使用的金额,满减、满折等',
  22. `discount` double comment '减免或折扣' ,
  23. PRIMARY KEY (`id`)
  24. ) comment '8.优惠券模板表';
  25. CREATE TABLE `t_usercoupon` (
  26. `id` int(11) AUTO_INCREMENT,
  27. `template_id` int(11) COMMENT '优惠券模板ID',
  28. `user_id` int(11) COMMENT '前端用户ID',
  29. `coupon_code` varchar(70) COMMENT '优惠券码',
  30. `assign_date` datetime COMMENT '优惠券分发时间',
  31. `status` int(11) COMMENT '优惠券状态',
  32. PRIMARY KEY (`id`)
  33. ) comment '9.用户优惠券表';

涉及技术栈:

Redis(1.缓存 2.分布式锁 3.倒计时)

RabbitMQ(1.削峰填谷,异步)

线程池(1.批处理,解决系统发放优惠券)

四、实现

基于微服务实现,优惠券服务

4.1 优惠券模板

1.新增优惠券活动接口

2.审核优惠券活动接口

优惠券服务 - 图16

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

优惠券服务 - 图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
}

优惠券服务 - 图18