开发了优惠促销功能,产品再也不汪汪叫了。。。
背景
大家好,我是程序员 cq。
最近快国庆节了,很多平台为了促销都开始发放优惠券,连上海都发放了足足 5 亿元的消费券,当知道这个消息的时候,我差点都忘了自己没钱的事了。
言归正传,我们 网站 也准备做一个优惠券的功能,正好借此机会给大家分享下我们是怎么做的优惠券,下面是某一次需求评审会的对话:
产品小 y:最近国庆节快到了,咱们网站也要像其他网站学习,给用户发放优惠券,把我们的价格打下来!
开发小 c:我才不做,你直接把会员价格下调不就好了?要啥优惠券,不做不做。
产品小 y:诶,你这个小同志,我下调价格是简单,但是我下调之后怎么统计有多少人看了我们的商品没买呢(转化率)?
开发小 c:你统计这个干什么,让用户感觉到优惠不就行了?
产品小 y:我统计转化率肯定是有意义的呀,我给不同的推广渠道发放不同的优惠券,可以得到不同渠道的转化率,如果有的渠道转化率很差,那我下次就不在这个渠道里推广了,有的渠道转化率很好,那我后面就要在这个渠道里加大推广量。还有就是我直接改价要手动改,假期人工改价格很难保证准时开始活动。
开发小 c:好像有道理啊,而且直接改价格也会让用户感觉平台的价格不稳定,价格经常变动,导致用户总是处于观望的状态。
技术方案
需求分析
既然上面需求评审确定了我们要做优惠券功能,那我们就要先梳理下我们要做什么东西。
首先就是优惠券的优惠能力,我们第一期就做的简单些,只用完成直减券,也就是购买会员的时候可以直接抵扣金额的消费券。当然实际上优惠券不光有直减券,还有满减券、折扣券等等,我们的网站也不需要那么复杂,能做一个直减券就够应对会员降价的功能了。
其次就是优惠券的开始时间和结束时间,便于控制优惠的开始时间和结束时间。
最后还有对应的使用条件,比如我们的优惠券只允许新用户使用,便于拉新,之类的。
那我们就可以确定这个优惠券只需要有减免价格、优惠券名称、开始结束时间以及使用条件就好了。
这时候网站运营同学听到我的喃喃低语,头突然伸过来。
他高呼:你这样可不行啊,光有这些可不够,我还要有优惠券的浏览量、领取量、使用量算转化率的嘞!
我心神一动,喔喔喔,那这样的话就可以做一个漏斗:浏览量 > 领取量 > 使用量,「使用量 / 领取量」就可以得到这个优惠券的转化率,也就知道了这个营销渠道的效果。
想到这里,我果断开口,好好好,就宠你。
具体实现
ok,我们确定了我们要做的内容,也就是要给会员商品做一个优惠券,其中要可以控制减免的金额、优惠券的浏览量、领取量、使用量、还有优惠券的开始时间、结束时间以及使用条件。
业务流程
明确了要做的需求,那么我们就可以确定下我们的整个业务流程:
技术实现
其实上述流程中最复杂的地方还是用户领取优惠券和使用优惠券购买后的处理逻辑,下面我带大家看下我们都是怎么做的。
首先我们要对领取优惠券做幂等,确保用户只在第一次领取优惠券的时候可以领取成功,后续的领取都返回已领取。要实现这个效果,我们需要对用户领取优惠券加锁,也就是下面这样:
String lockKey = "coupon_receive_lock:" + userId + ":" + couponId;
synchronized (lockKey.intern()) {
couponService.receiveCouponInner(loginUser, coupon);
}
但是由于我们公司是一个分布式的服务,所以本地锁其实是无效的,正好我们的服务用到了 redis,而且还引入了 redisson,那么就可以直接使用 redisson 的分布式锁,
String lockKey = RedisKeyConstant.COUPON_RECEIVE_LOCK + userId + ":" + couponId;
RLock lock = redissonClient.getLock(lockKey);
try {
lock.lock();
return couponService.receiveCouponInner(loginUser, coupon);
} finally {
lock.unlock();
}
但是这样写就太 low 了,我不如直接封装一个方法,让后续再使用分布式锁更方便:
public T blockExecute(String lockKey, Supplier supplier) {
RLock lock = redissonClient.getLock(LOCK_KEY_PREFIX + lockKey);
try {
lock.lock();
// 执行方法
return supplier.get();
} finally {
lock.unlock();
}
}
这样,我们在使用分布式锁的时候就更方便了:
String lockKey = RedisKeyConstant.COUPON_RECEIVE_LOCK + userId + ":" + couponId;
return redisLockUtil.blockExecute(lockKey,
() -> couponService.receiveCouponInner(loginUser, coupon));
很好,在完成需求的时候又做了个基建工作,不愧是我。
其次就是在用户领取优惠券之后,我们的优惠券库存就应该对应的 -1,同时领取量对应 +1,那么这个 sql 就是这样的:
update coupon set leftNum = leftNum - 1, receiveNum = receiveNum + 1 where id = ?
这里可以确定数据不会出现混乱,因为在执行更新操作的时候,mysql 会有行锁,所以所有领取的用户都会在这里进行排队操作。
但是这样反而会出现问题,如果一瞬间领取优惠券的用户量激增,大家都在排队等库存量 - 1,就导致领取优惠券的时候用户会感觉很卡,也就是我们常说的热点行问题,不过像很多云厂商,都会针对热点行进行优化,比如某某云的 Inventory Hint 就可以实现快速提交 / 回滚事务,提高吞吐量,详细文章可以看下这篇文章:Inventory Hint,如果并发量真的很大的话,可以考虑用 redis 实现库存的 - 1,不过按照以往的经验来说不用 redis 也可以扛得住,这里就没必要再搞那么复杂了。
最后在领取优惠券之后,我们要接收来自支付中心的回调,根据用户的支付信息给用户赋予对应的会员权限,同时设置用户领取的优惠券为已使用,这样用户就不能再使用这个优惠券了,同时我们也可以关联查询到优惠券的使用量让运营同学进行分析。
了解领取优惠券的具体实现之后,后面的开发就简单了,这里就不再细说了,属于是商业机密了(其实就是代码写的太烂)。
优惠券到底带来了什么
优惠券的技术方案完成后,我们就应该深思一下,优惠券对我们来说到底有什么用,可以给我们带来什么好处?
对于用户而言自然是比之前便宜了许多,更省钱;用户觉得自己赚了,性价比高。
对于运营而言,可以对运营效果进行评估,比如销售变化、客户流失、获客成本等进而调整后续的营销活动;查看不同营销渠道表现,确定不同营销渠道优惠券效果;还可以利用历史数据预测未来的销售趋势,就像我们国庆节的优惠券就是参考了去年的优惠券使用情况来定的。
这也就是为什么运营同学会对优惠券这么痴迷了。
最后
通过这个优惠券功能的开发,其实大家也发现了,从最开始的直接改价到最后的优惠券,我们要做的不光是对商品的减免,还要做运营相关的功能。所以对于开发者而言,很多功能其实不仅要关注代码怎么写,更要考虑实现这个功能的意义,如果没意义,我就不用做了(bushi)。
了解功能实现的意义之后,我们身为开发就应该本能的想到,如何确定这个功能有多少人用,如何埋点,也就是为后续的运营计划以及后续的开发计划做准备。
来源:juejin.cn/post/7419598962346328105