第九天 优惠卷管理

目录

优惠卷原型分析

兑换码

新增优惠卷

兑换码生成算法

​编辑 ​编辑

Base32

重兑校验算法

防刷校验算法

异步生成兑换码

面试


优惠卷原型分析

推广方式就是用户手动去领还是使用兑换码去兑换

优惠卷规则可以使用优惠卷类型去判断

  • 优惠类型:天机学堂支持的类型有 1:满减,2:每满减,3:折扣,4:无门槛

  • 优惠券状态:包括 1:待发放,2:未开始 3:进行中,4:已结束,5:暂停

使用返回 可以存0 1 适用范围 在另外一张表中存储

其中的字段包含:

  • 优惠券名称:一个普通字符串

  • 使用范围:这里有两种选择:全部课程、指定课程分类,也就是不限定课程、限定课程,可以用布尔类型来表示。不过一旦选定了课程分类,就需要指定真正限定的分类。

      此处是允许多选的,也就是说一个优惠券可以限定多个课程分类。而一个分类也可能被不同的券作为限定范围。因此优惠券与限定的分类是多对多关系。需要一张中间表来保存关系。这个以后再说。

  • 优惠券类型:包含满减、每满减、满折扣、无门槛四种,例如:

    • 满100减15

    • 每满100减10

    • 满200打8折,不超过50

    • 直减20

可以看出来,虽然规则不同,但都可以用以下几部分来表示:

  • 优惠的门槛:比如满100的100

  • 优惠值:比如减15的15、打8折的8

  • 优惠上限:比如不超过50

  因此,我们完全要表示完整优惠策略就需要四个字段:优惠类型、优惠门槛、优惠值、优惠上限

  • 推广方式:手动领取和指定发放

  • 发放数量

  • 每人限领数量

与新增页面重复的就不再赘述了,这里多出的一些字段有:

  • 已领取数量

  • 已使用数量

  • 领用期限:也就是优惠券开始发放、结束发放的时间

  • 使用期限:用户领取券后的使用期限,有两种方式:

  • 固定时间段:需要指定开始时间、结束时间

  • 固体天数:指定天数,从用户领取之日起计算

  • 优惠券状态

-- 导出  表 tj_promotion.coupon 结构
CREATE TABLE IF NOT EXISTS `coupon` (
  `id` bigint NOT NULL AUTO_INCREMENT COMMENT '优惠券id',
  `name` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL DEFAULT '0' COMMENT '优惠券名称,可以和活动名称保持一致',
  `type` tinyint NOT NULL DEFAULT '1' COMMENT '优惠券类型,1:普通券。目前就一种,保留字段',
  `discount_type` tinyint NOT NULL COMMENT '折扣类型,1:满减,2:每满减,3:折扣,4:无门槛',
  `specific` bit(1) NOT NULL DEFAULT b'0' COMMENT '是否限定作用范围,false:不限定,true:限定。默认false',
  `discount_value` int NOT NULL DEFAULT '1' COMMENT '折扣值,如果是满减则存满减金额,如果是折扣,则存折扣率,8折就是存80',
  `threshold_amount` int NOT NULL DEFAULT '0' COMMENT '使用门槛,0:表示无门槛,其他值:最低消费金额',
  `max_discount_amount` int NOT NULL DEFAULT '0' COMMENT '最高优惠金额,满减最大,0:表示没有限制,不为0,则表示该券有金额的限制',
  `obtain_way` tinyint NOT NULL DEFAULT '0' COMMENT '获取方式:1:手动领取,2:兑换码',
  `issue_begin_time` datetime DEFAULT NULL COMMENT '开始发放时间',
  `issue_end_time` datetime DEFAULT NULL COMMENT '结束发放时间',
  `term_days` int NOT NULL DEFAULT '0' COMMENT '优惠券有效期天数,0:表示有效期是指定有效期的',
  `term_begin_time` datetime DEFAULT NULL COMMENT '优惠券有效期开始时间',
  `term_end_time` datetime DEFAULT NULL COMMENT '优惠券有效期结束时间',
  `status` tinyint DEFAULT '1' COMMENT '优惠券配置状态,1:待发放,2:未开始   3:进行中,4:已结束,5:暂停',
  `total_num` int NOT NULL DEFAULT '0' COMMENT '总数量,不超过5000',
  `issue_num` int NOT NULL DEFAULT '0' COMMENT '已发行数量,用于判断是否超发',
  `used_num` int NOT NULL DEFAULT '0' COMMENT '已使用数量',
  `user_limit` int NOT NULL DEFAULT '1' COMMENT '每个人限领的数量,默认1',
  `ext_param` json DEFAULT NULL COMMENT '拓展参数字段,保留字段',
  `create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
  `update_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
  `creater` bigint NOT NULL COMMENT '创建人',
  `updater` bigint NOT NULL COMMENT '更新人',
  PRIMARY KEY (`id`) USING BTREE
) ENGINE=InnoDB AUTO_INCREMENT=1630563495906942979 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci COMMENT='优惠券的规则信息';

兑换码

兑换码的作用是让用户拿着这个码来兑换一张优惠券。因此一定与两个实体有关:

  • 优惠券

  • 用户

也就是说,我们需要知道将来是来兑换的券,可以兑换哪张券。

新增优惠卷

 重点是这里面的stream流的用法

兑换码生成算法

兑换码是用来兑换优惠卷的,32BIT的自增id 权重与32位加权作为秘钥,多种权重值,4位新鲜值来判断选择作为权重进行加权,加权得出来的数就是签名

 

要求如下:

  • 可读性好:兑换码是要给用户使用的,用户需要输入兑换码,因此可读性必须好。我们的要求:

    • 长度不超过10个字符

    • 只能是24个大写字母和8个数字:ABCDEFGHJKLMNPQRSTUVWXYZ23456789

  • 数据量大:优惠活动比较频繁,必须有充足的兑换码,最好有10亿以上的量

  • 唯一性:10亿兑换码都必须唯一,不能重复,否则会出现兑换混乱的情况

  • 不可重兑:兑换码必须便于校验兑换状态,避免重复兑换

  • 防止爆刷:兑换码的规律性不能很明显,不能轻易被人猜测到其它兑换码

  • 高效:兑换码生成、验证的算法必须保证效率,避免对数据库带来较大的压力

要满足唯一性,很多同学会想到以下技术:

  • UUID

  • Snowflake

  • 自增id

我们的兑换码要求是24个大写字母和8个数字。而以上算法最终生成的结果都是数值类型,并不符合我们的需求!

有没有什么办法,可以把数字转为我们要求的格式呢?

Base32

  • Base32编码是使用32个可打印字符(字母A-Z和数字2-7)对任意字节数据进行编码的方案,编码后的字符串不用区分大小写并排除了容易混淆的字符,可以方便地由人类使用并由计算机处理。
  •    Base32主要用于编码二进制数据,但是Base32也能够编码诸如ASCII之类的二进制文本。
  •    Base32将任意字符串按照字节进行切分,并将每个字节对应的二进制值(不足8比特高位补0)串联起来,按照5比特一组进行切分,并将每组二进制值转换成十进制来对应32个可打印字符中的一个。

那自增id算法符合我们的需求呢?

自增id从1增加到Integer的最大值,可以达到40亿以上个数字,而占用的字节仅仅4个字节,也就是32个bit位,距离50个bit位的限制还有很大的剩余,符合要求!

综上,我们可以利用自增id作为兑换码,但是要利用Base32加密,转为我们要求的格式。此时就符合了我们的几个要求了:

  • 可读性好:可以转为要求的字母和数字的格式,长度还不超过10个字符

  • 数据量大:可以应对40亿以上的数据规模

  • 唯一性:自增id,绝对唯一

重兑校验算法

那重兑问题该如何判断呢?此处有两种方案:

  • 基于数据库:我们在设计数据库时有一个字段就是标示兑换码状态,每次兑换时可以到数据库查询状态,避免重兑。

    • 优点:简单

    • 缺点:对数据库压力大

  • 基于BitMap:兑换或没兑换就是两个状态,对应0和1,而兑换码使用的是自增id.我们如果每一个自增id对应一个bit位,用每一个bit位的状态表示兑换状态,是不是完美解决问题。而这种算法恰好就是BitMap的底层实现,而且Redis中的BitMap刚好能支持2^32个bit位。

    • 优点:简答、高效、性能好

    • 缺点:依赖于Redis现在,就剩下防止爆刷了。我们的兑换码规律性不能太明显,否则很容易被人猜测到其它兑换码。但是,如果我们使用了自增id,那规律简直太明显了,岂不是很容易被人猜到其它兑换码?!

  • 所以,我们采用自增id的同时,还需要利用某种校验算法对id做加密验证,避免他人找出规律,猜测到其它兑换码,甚至伪造、篡改兑换码。

    那该采用哪种校验算法呢?

防刷校验算法

非常可惜,没有一种现成的算法能满足我们的需求,我们必须自己设计一种算法来实现这个功能。

不过大家不用害怕,我们可以模拟其它验签的常用算法。比如大家熟悉的JWT技术。我们知道JWT分为三部分组成:

  • Header:记录算法

  • Payload:记录用户信息

  • Verify Signature:验签,用于验证整个token

JWT中的的Header和Payload采用的是Base64算法,与我们Base32类似,几乎算是明文传输,难道不怕其他人伪造、篡改token吗?

为了解决这个问题,JWT中才有了第三部分,验证签名。这个签名是有一个秘钥,结合Header、Payload,利用MD5或者RSA算法生成的。因此:

  • 只要秘钥不泄露,其他人就无法伪造签名,也就无法伪造token。

  • 有人篡改了token,验签时会根据header和payload再次计算签名。数据被篡改,计算的到的签名肯定不一致,就是无效token

因此,我们也可以模拟这种思路:

  • 首先准备一个秘钥

  • 然后利用秘钥对自增id做加密,生成签名

  • 将签名、自增id利用Base32转码后生成兑换码

只要秘钥不泄露,就没有人能伪造兑换码。只要兑换码被篡改,就会导致验签不通过

 

异步生成兑换码

开启异步调用需要两个注解:启动类上@EnaleAsync  方法上@Async

异步用的是线程池里面的线程 ,2.0.9之后用的是threadpoolTaskExecutor中的注解,但是默认的线程池线程队列长度是int的最大值,会有OOM风险,所以最好重写

@Slf4j
@Configuration
public class PromotionConfig {

    @Bean
    public Executor generateExchangeCodeExecutor(){
        ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
        // 1.核心线程池大小
        executor.setCorePoolSize(2);
        // 2.最大线程池大小
        executor.setMaxPoolSize(5);
        // 3.队列大小
        executor.setQueueCapacity(200);
        // 4.线程名称
        executor.setThreadNamePrefix("exchange-code-handler-");
        // 5.拒绝策略
        executor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy());
        executor.initialize();
        return executor;
    }

 然后在注解后面指明用的哪个线程池

面试中文那个地方用到线程池?

异步生成兑换码,延迟队列中拉取到任务后,保存数据库任务的执行(提交学习记录到数据库)

mybatisplus中对应数据库的三种主要主键策略

auto  自增,此时是以数据库的自增id为主,这个时候数据库主键必须是自增的

input  这个时候是以手动输入为主

雪花算法

redis incr和incrBy的使用

用incr和incrBy在接口里做了下埋点统计每天请求总数,这两个命令还是挺好用的,先说下这俩命令吧
注:redis后台服务是串行的单线程执行,不存在并发,即多线程调用Incr/incrby方法,在redis服务器上仍然是串行的单线程执行,不存在并发,所以这俩命令都是原子自增、线程安全的。

Redis Incr 命令将 key 中储存的数字值增一。

如果 key 不存在,那么 key 的值会先被初始化为 0 ,然后再执行 INCR 操作。

Redis Incrby 命令将 key 中储存的数字加上指定的增量值。

如果 key 不存在,那么 key 的值会先被初始化为 0 ,然后再执行 INCRBY 命令。

keyi 

使用redis-incr实现流量控制

这里我们将在java中使用redis-incr的特性来构建一个1分钟内只允许 请求100次的控制代码,key代表在redis内存放的被控制的键值。 

面试

面试官:你们优惠券支持兑换码的方式是吧,哪兑换码是如何生成的呢?(请设计一个优惠券兑换码生成方案,可以支持20亿以上的唯一兑换码,兑换码长度不超过10,只能包含字母数字,并且要保证生成和校验算法的高效)

不需要数据库交互,效率很高 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值