优惠券系统-第二章介绍
第二章主要是介绍优惠券的领取、补券。
领取流程
优惠券的领取,是一个单独的接口,作为一个服务给应用层调用。比如,用户注册送几张券,用户首单支付送券,等等。像“领券中心”,用户可以直接点击领取的券,是通过“某一个活动”领券,有活动应用层调用送券服务,第三章会详细介绍活动。
生成的券,不可能全部加载到缓存中,比如20万个券可以每5000个加入缓存,领取完毕后,再向缓存中补券。当调用方取不到券时,直接返回一个指定错误code码,然后系统触发异步补券,向缓存补充5000个。调用方就需要一定的重试原则,第一次没有领取成功时,需要做重试。
流程图如下图所示
下面给出从缓存队列中拿券的代码,仅是从缓存中取得一个券。
public CouponInfoVo getCouponInfo(ReceiveCouponRequest request){
try {
//从redis队列拿出一个券
String couponSn = redisOperations.rpop(PromotionCacheConstant.getCacheCouponQueueKey(request.getActSn()));
if(!Check.NuNStr(couponSn)){
//从缓存中取出券的详情
CouponBaseEntity couponBaseEntity = null;
String couponBaseStr = redisOperations.get(couponSn);
CouponInfoVo infoVo = new CouponInfoVo();
BeanUtils.copyProperties(couponBaseEntity, infoVo);
//将这个券从缓存中删除
redisOperations.del(couponSn);
return infoVo;
} else {
//如果缓存中没券,异步触发补充券
taskExecutor.execute(()->fillCacheCouponSnQueue(request.getActSn()));
}
}catch (Exception e){
LogUtil.error(logger,"获取优惠券信息失败,error:{}",e);
return null;
}
return null;
}
下面给出领券的代码,会调用上面的拿券方法,然后更新券状态,记录领取人与券的关系。
public DataTransferObject<Void> receiveCoupon(ReceiveCouponRequest receiveCouponRequest){
try{
//这里从缓存中key判断这类券是否被领取完毕,actSn号为某一类型券
String couponQueueStatus = redisOperations.get(PromotionCacheConstant.getCacheCouponOverKey(request.getActSn()));
if(!Check.NuNStr(couponQueueStatus) && couponQueueStatus.equals(PromotionCacheConstant.COMMON_LABEL)){
//已被领取完毕
LogUtil.warn(logger, "优惠券队列已消费完("+request.getActSn()+")");
return null;
}
//这里调用了上面的拿券方法
CouponInfoVo couponInfo = couponBaseServiceImpl.getCouponInfo(receiveCouponRequest);
if (!Check.NuNObj(couponInfo)) {
//拿到券
receiveCouponRequest.setActSn(couponInfo.getActSn());
receiveCouponRequest.setCouponSn(couponInfo.getCouponSn());
//这个方法更新券状态,同时记录领取信息,需要在一个实务中
dto = couponUserService.saveReceiveCoupon(receiveCouponRequest);
if(!dto.checkSuccess()){
LogUtil.error(LOGGER,"未领取到优惠券,param:{}",
JsonEntityTransform.Object2Json(dto));
return dto;
}
} else {
dto.setMsg("未领取到优惠券");
//标识领取失败,但可以重试
dto.setErrCode(PromotionConstant.RECEIVE_COUPON_TRY_CODE);
}
} catch (Exception e) {
LogUtil.error(LOGGER,"领券异常,{}",e);
dto.setErrorMsg("领券异常");
return dto;
}
return dto;
}
有了上面的两端代码大家应该能明白这个领取流程了,下面还有最后一段的异步“补券”代码,当缓存丢列中券不够时,代码如下
public void fillCacheCouponSnQueue(String actSn){
try{
//查询5000个券出来
CouponActivityQueryRequest couponActivityQueryRequest = new CouponActivityQueryRequest();
couponActivityQueryRequest.setLimit(5000);
couponActivityQueryRequest.setActSn(actSn);
List<CouponBaseEntity> unusedCouponBaseList = getAllUnusedCouponSn(couponActivityQueryRequest);
//如果查不到券,说明被领完了
if(Check.NuNCollection(unusedCouponBaseList)){
//放入缓存该类券被领完的key
redisOperations.set(actSn,"1");
LogUtil.warn(logger, "可用优惠券已使用完("+actSn+")");
}
//数据库中还有券时
if(!Check.NuNCollection(unusedCouponBaseList)){
String couponBaseQueueKey = PromotionCacheConstant.getCacheCouponQueueKey(actSn);
unusedCouponBaseList.parallelStream().forEach(base->{
//将券加入缓存中
redisOperations.set(base.getCouponSn(), JsonEntityTransform.Object2Json(base));
redisOperations.lpush(couponBaseQueueKey,base.getCouponSn());
//修改券已经被加入到缓存的状态
CouponBaseEntity couponBase = new CouponBaseEntity();
couponBase.setCouponSn(base.getCouponSn());
couponBase.setLoadStatus(1);
couponBaseDao.updateCouponBase(couponBase);
});
}
} catch (Exception e) {
LogUtil.error(logger,"填充优惠券码队列失败,error:{}",e);
}
}
代码已经全部给出,在这里说明一下,为什么当数据库券全部被领完后,需要缓存中放一个标key,因为领券往往是并发量比较大的操作,能从缓存判断,尽量从缓存判断。
相关定时任务
领取券被领取后,就会有过期时间,优惠券过期状态通过定时任务来修改,通过定时任务修改的话就会有一定的延时,券本身过期时间已经过了,定时任务确没有执行,还处于未过期的状态。所以在可用优惠券列表的接口层面要将这种,已经过期,但是状态未及时更新的券剔除掉。
第二章主要介绍了优惠券领取、补券,第三章介绍活动中心。