1、前言
微信小程序直播是微信官方提供的商家经营工具,商家可通过在小程序内直播实现用户互动与商品销售的闭环,无需任何的跳转,提高下单转化率,直播更是成为链接商家和消费者的重要销售渠道!
小程序直播具备评论、点赞、连麦、拍一拍等丰富的互动功能,抽奖、优惠券等高效的营销功能,以及成员管理、评论管理、推流直播、数据看板等完善商家工具。通过引入小程序直播组件,商家自有小程序可快速具备直播能力,提升经营效率。
虽然有抽奖,优惠券的营销功能,但是却没有红包功能,如果有红包功能,增加了和用户的互动,更能吸引用户留下来观看直播。其实,我们是可以自己在直播间开发红包功能的。当然,要实现这个功能,小程序要先开通直播权限,开通直播权限需满足小程序近90天内有过支付行为,如果因为这个无法开通的联系我,可以快速开通。
2、思路
说一下这个功能实现的思路,首先后台做一个录红包的菜单,字段包括主播名称、主播头像、标语(恭喜发财,大吉大利)、有效时间、红包金额、红包个数、剩余现金红包金额、剩余现金红包个数、创建时间、版本号(乐观锁),还要有一个抢红包记录表,字段包括红包id、抢到红包用户的id、抢到红包用户的名称、抢到红包用户的头像、抢到的红包金额、创建时间。然后去小程序直播后台录商品,商品路径字段填写要跳转的小程序红包页面路径,需要在后面拼接红包id参数,比如像这样,
pages/redPacket/redPacket.html?redPacketId=123456
当用户在直播页面点击该商品进入红包页面,前端就可以拿到红包id传给后台接口,查到该红包的相关信息,做各种操作了,比如生成随机金额,扣减红包金额和个数等等。这个需要主播引导用户做好抢红包的准备,然后直播间助理通过上架商品来显示红包商品。
思路很简单,代码实现起来也很简单,但是我们需要考虑几个问题,
1、抢红包就像秒杀商品一样,是拼手速的,要考虑并发,不能出现超卖(这里是超抢)的现象,不然亏的是老板的💰,就该找你聊天了。这里我们采用简单的乐观锁来解决这个问题。
2、如果乐观锁更新失败,直接返回给用户提示抢到空红包或其他友好提示也是可以的,但是如果我们增加重试机制的话,体验会更好点。这里我们采用注解的方式进行重试,默认重试3次。
3、实现
我们先来实现注解重试的功能,且看代码如下
/**
* 定义重试机制注解
*/
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public [@interface](/user/interface) ApiRetry {
/**
* 默认次数
* [@return](/user/return)
*/
int value() default 3;
}
/**
* 定义一个重试机制切面类
*/
[@Slf](/user/Slf)4j
[@Aspect](/user/Aspect)
[@Component](/user/Component)
public class RetryAspect {
[@Pointcut](/user/Pointcut)("[@annotation](/user/annotation)(com.redPacket.common.apiIdempotent.annotation.ApiRetry)")
public void retryPointcut() {
}
[@Around](/user/Around)("retryPointcut() && [@annotation](/user/annotation)(retry)")
[@Transactional](/user/Transactional)(isolation = Isolation.READ_COMMITTED)
public Object tryAgain(ProceedingJoinPoint joinPoint, ApiRetry retry) throws Throwable {
int count = 0;
do {
count++;
try {
return joinPoint.proceed();
} catch (ApiRetryException e) {
if (count > retry.value()) {
log.error("重试失败!");
TransactionAspectSupport.currentTransactionStatus().setRollbackOnly();
throw new ApiRetryException("当前用户较多,请稍后重试");
} else {
log.info("=====正在重试,第{}次=====",count);
}
}
} while (true);
}
}
/**
* 定义一个重试机制的异常
*/
public class ApiRetryException extends RuntimeException {
private static final long serialVersionUID = 1L;
private Integer status = 0;
private String msg;
public ApiRetryException(String msg) {
super(msg);
this.msg = msg;
}
public ApiRetryException(String msg, Throwable e) {
super(msg, e);
this.msg = msg;
}
public ApiRetryException(String msg, Integer status) {
super(msg);
this.msg = msg;
this.status = status;
}
public ApiRetryException(String msg, Integer status, Throwable e) {
super(msg, e);
this.msg = msg;
this.status = status;
}
public Integer getStatus() {
return status;
}
public void setStatus(Integer status) {
this.status = status;
}
public String getMsg() {
return msg;
}
public void setMsg(String msg) {
this.msg = msg;
}
}
/**
* 定义异常处理器
*/
[@RestControllerAdvice](/user/RestControllerAdvice)
public class RestExceptionHandler {
private Logger logger = LoggerFactory.getLogger(getClass());
/**
* 处理重试机制异常
*/
[@ExceptionHandler](/user/ExceptionHandler)(ApiRetryException.class)
public JsonModel handleApiRetryException(ApiRetryException e){
JsonModel jsonModel = new JsonModel();
jsonModel.setStatus(e.getStatus());
jsonModel.setMsg(e.getMsg());
return jsonModel;
}
}
关于红包接口就三个,
1、给前端判断是弹出抢红包的窗口还是弹出其他提示窗口(你已抢过该红包了、手慢了,红包已过期、手慢了,红包派完了)。
/**
* 进入红包页面判断是否可以抽红包
* [@param](/user/param) redPacketId
* [@param](/user/param) userId
* [@return](/user/return)
*/
[@Override](/user/Override)
public JsonModel intoRedPacket(String redPacketId, String userId) {
ChatroomRedPacketEntity redPacket = baseMapper.selectById(redPacketId);
if (redPacket == null) {
throw new JsonModelException("id为【"+redPacketId+"】的红包不存在");
}
ChatroomRedPacketRecordEntity hasRecord = chatroomRedPacketRecordService.getOne(Wrappers.<ChatroomRedPacketRecordEntity>lambdaQuery()
.eq(ChatroomRedPacketRecordEntity::getRedPacketId, redPacketId)
.eq(ChatroomRedPacketRecordEntity::getUserId, userId));
if (hasRecord != null) {
return JsonModel.toFail(10001,"你已抢过该红包了");
}
redPacket = baseMapper.selectOne(Wrappers.<ChatroomRedPacketEntity>lambdaQuery()
.eq(ChatroomRedPacketEntity::getId,redPacketId)
.apply("date_add(create_date, interval valid_time hour) >= current_timestamp"));
if (redPacket == null) {
return JsonModel.toFail(10002,"手慢了,红包已过期");
}
if (redPacket.getCashNum()+redPacket.getCouponNum() <= 0) {
return JsonModel.toFail(10003,"手慢了,红包派完了");
}
return JsonModel.toSuccess(200,"弹出抽红包窗口");
}
[@ApiOperation](/user/ApiOperation)(value = "进入红包页面判断是否可以抽红包")
[@ApiImplicitParams](/user/ApiImplicitParams)({
@ApiImplicitParam(name = "redPacketId", value = "红包id", required = true, paramType = "body", dataType = "String"),
@ApiImplicitParam(name = "userId", value = "用户id", required = true, paramType = "body", dataType = "String")
})
[@PostMapping](/user/PostMapping)("/intoRedPacket/{redPacketId}/{userId}")
public JsonModel intoRedPacket([@PathVariable](/user/PathVariable)("redPacketId")String redPacketId,
[@PathVariable](/user/PathVariable)("userId")String userId) {
return chatroomRedPacketService.intoRedPacket(redPacketId,userId);
}
2、这个是重中之重,就是点击抢红包的接口。
/**
* 抽红包
* [@param](/user/param) redPacketId
* [@param](/user/param) userId
* [@return](/user/return)
*/
[@Override](/user/Override)
[@ApiRetry](/user/ApiRetry)
[@Transactional](/user/Transactional)(rollbackFor = Exception.class)
public JsonModel grabRedPacket(String redPacketId, String userId) {
ChatroomRedPacketEntity redPacket = baseMapper.selectById(redPacketId);
if (redPacket == null) {
throw new JsonModelException("id为【"+redPacketId+"】的红包不存在");
}
ChatroomRedPacketRecordEntity hasRecord = chatroomRedPacketRecordService.getOne(Wrappers.<ChatroomRedPacketRecordEntity>lambdaQuery()
.eq(ChatroomRedPacketRecordEntity::getRedPacketId, redPacketId)
.eq(ChatroomRedPacketRecordEntity::getUserId, userId));
if (hasRecord != null) {
return JsonModel.toFail(10001,"你已抢过该红包了");
}
redPacket = baseMapper.selectOne(Wrappers.<ChatroomRedPacketEntity>lambdaQuery()
.eq(ChatroomRedPacketEntity::getId,redPacketId)
.apply("date_add(create_date, interval valid_time hour) >= current_timestamp"));
if (redPacket == null) {
return JsonModel.toFail(10002,"手慢了,红包已过期");
}
if (redPacket.getCashNum()+redPacket.getCouponNum() <=0) {
return JsonModel.toFail(10003,"手慢了,红包派完了");
}
long money = 0;
long restCashNum = redPacket.getRestCashNum();
long restCashAmount = redPacket.getRestCashAmount().longValue();
if (restCashNum >= 1) {
restCashNum = restCashNum - 1;
if (restCashNum == 0) {
money = restCashAmount;
} else {
money = ThreadLocalRandom.current().nextInt((int) (restCashAmount / (restCashNum+1) * 2 - 1)) + 1;
}
restCashAmount = restCashAmount - money;
}
result.put("money",money);
// 更新红包剩余个数和剩余金额
boolean isUpdate = chatroomRedPacketService.update(Wrappers.<ChatroomRedPacketEntity>lambdaUpdate()
.set(ChatroomRedPacketEntity::getRestCashNum,restCashNum)
.set(ChatroomRedPacketEntity::getRestCashAmount,restCashAmount)
.set(ChatroomRedPacketEntity::getVersion,redPacket.getVersion() + 1)
.eq(ChatroomRedPacketEntity::getId,redPacket.getId())
.eq(ChatroomRedPacketEntity::getVersion,redPacket.getVersion()));
optimisticHandler(redPacket,isUpdate,userId,money);
return JsonModel.toSuccess(result);
}
private void optimisticHandler(ChatroomRedPacketEntity redPacket,boolean isUpdate,
String userId,long money)
if (!isUpdate) {
throw new ApiRetryException("更新失败,开始重试");
} else {
// 入库抽红包记录
UserEntity user = userService.getById(userId);
ChatroomRedPacketRecordEntity record = new ChatroomRedPacketRecordEntity();
record.setRedPacketId(redPacket.getId().toString())
.setUserId(userId)
.setAmount(new BigDecimal(money))
.setCreateDate(new Date())
.setUserName(user.getNickName())
.setUserAvatar(user.getAvatarImageId());
chatroomRedPacketRecordService.save(record);
// 这里写其他业务逻辑,比如给用户账上增加相应的红包金额,可以提现
}
}
3、最后一个是抢红包记录列表。
/**
* 抽红包记录
* [@param](/user/param) redPacketId
* [@return](/user/return)
*/
[@Override](/user/Override)
public JsonModel redPacketRecord(String redPacketId) {
ChatroomRedPacketEntity redPacket = baseMapper.selectById(redPacketId);
if (redPacket == null) {
throw new JsonModelException("id为【"+redPacketId+"】的红包不存在");
}
Map<String,Object> result = new HashMap<>();
//红包信息
result.put("redPacket",redPacket);
List<ChatroomRedPacketRecordEntity> recordList = chatroomRedPacketRecordService.list(Wrappers.<ChatroomRedPacketRecordEntity>lambdaQuery()
.eq(ChatroomRedPacketRecordEntity::getRedPacketId, redPacketId)
.ne(ChatroomRedPacketRecordEntity::getAmount, 0)
.orderByDesc(ChatroomRedPacketRecordEntity::getCreateDate));
//领红包记录
result.put("recordList",recordList);
return JsonModel.toSuccess(result);
}
[@ApiOperation](/user/ApiOperation)(value = "抽红包记录")
[@ApiImplicitParams](/user/ApiImplicitParams)({
@ApiImplicitParam(name = "redPacketId", value = "红包id", required = true, paramType = "body", dataType = "String"),
@ApiImplicitParam(name = "userId", value = "用户id", required = true, paramType = "body", dataType = "String")
})
[@PostMapping](/user/PostMapping)("/redPacketRecord/{redPacketId}")
public JsonModel redPacketRecord([@PathVariable](/user/PathVariable)("redPacketId")String redPacketId) {
return chatroomRedPacketService.redPacketRecord(redPacketId)
}
4、总结
思路就是那样的思路,至于实现方式,代码逻辑有很多种,可以自行实现。有其他方案的同学可以评论区一起交流下^_^。