feat(market):新增缓存库存和用户端抢卷的功能
This commit is contained in:
parent
cea88fc351
commit
c09e6bf680
@ -2,38 +2,22 @@ package com.jzo2o.market.config;
|
||||
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.context.annotation.Primary;
|
||||
import org.springframework.core.io.ClassPathResource;
|
||||
import org.springframework.data.redis.connection.RedisConnectionFactory;
|
||||
import org.springframework.data.redis.core.RedisTemplate;
|
||||
import org.springframework.data.redis.core.script.DefaultRedisScript;
|
||||
import org.springframework.data.redis.serializer.GenericJackson2JsonRedisSerializer;
|
||||
import org.springframework.data.redis.serializer.GenericToStringSerializer;
|
||||
import org.springframework.data.redis.serializer.SerializationException;
|
||||
import org.springframework.data.redis.serializer.StringRedisSerializer;
|
||||
import org.springframework.scripting.support.ResourceScriptSource;
|
||||
|
||||
import java.util.Objects;
|
||||
|
||||
/**
|
||||
* 加载Redis的Lua脚本配置类
|
||||
* @author JIAN
|
||||
*/
|
||||
@Configuration
|
||||
public class RedisLuaConfiguration {
|
||||
|
||||
@Bean("seizeCouponScript")
|
||||
public DefaultRedisScript<Integer> seizeCouponScript() {
|
||||
DefaultRedisScript<Integer> redisScript = new DefaultRedisScript<>();
|
||||
//resource目录下的scripts文件下的seizeCouponScript.lua文件
|
||||
// resource目录下的scripts文件下的seizeCouponScript.lua文件
|
||||
redisScript.setScriptSource(new ResourceScriptSource(new ClassPathResource("scripts/seizeCouponScript.lua")));
|
||||
redisScript.setResultType(Integer.class);
|
||||
return redisScript;
|
||||
}
|
||||
|
||||
@Bean("lua_test01")
|
||||
public DefaultRedisScript<Integer> getLuaTest01() {
|
||||
DefaultRedisScript<Integer> redisScript = new DefaultRedisScript<>();
|
||||
//resource目录下的scripts文件下的lua_test01.lua文件
|
||||
redisScript.setScriptSource(new ResourceScriptSource(new ClassPathResource("scripts/lua_test01.lua")));
|
||||
redisScript.setResultType(Integer.class);
|
||||
return redisScript;
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
@ -3,31 +3,35 @@ package com.jzo2o.market.controller.consumer;
|
||||
import com.jzo2o.common.expcetions.BadRequestException;
|
||||
import com.jzo2o.common.utils.ObjectUtils;
|
||||
import com.jzo2o.market.enums.CouponStatusEnum;
|
||||
import com.jzo2o.market.model.dto.request.SeizeCouponReqDTO;
|
||||
import com.jzo2o.market.model.dto.response.CouponSimpleInfoResDTO;
|
||||
import com.jzo2o.market.service.IActivityService;
|
||||
import com.jzo2o.market.service.ICouponService;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RequestParam;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
import javax.annotation.Resource;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 运营端 - 优惠卷管理控制器
|
||||
* 用户端 - 优惠卷管理控制器
|
||||
* @author JIAN
|
||||
*/
|
||||
@Slf4j
|
||||
@RestController("consumerCouponController")
|
||||
@RequestMapping("/consumer/coupon")
|
||||
public class CouponController {
|
||||
@Resource
|
||||
private IActivityService activityService;
|
||||
@Resource
|
||||
private ICouponService couponService;
|
||||
|
||||
/**
|
||||
* 获取当前用户的优惠卷(滚动查询)
|
||||
*/
|
||||
@GetMapping("/my")
|
||||
public List<CouponSimpleInfoResDTO> currentUserCoupon(@RequestParam Integer status,
|
||||
@RequestParam(required = false) Integer lastId) {
|
||||
@RequestParam(required = false) Integer lastId) {
|
||||
CouponStatusEnum couponStatusEnum = CouponStatusEnum.statusOf(status);
|
||||
if (ObjectUtils.isEmpty(couponStatusEnum)) {
|
||||
throw new BadRequestException("优惠卷状态出错");
|
||||
@ -35,4 +39,12 @@ public class CouponController {
|
||||
|
||||
return couponService.getCurrentUserCoupon(couponStatusEnum, lastId);
|
||||
}
|
||||
|
||||
/**
|
||||
* 用户抢卷接口
|
||||
*/
|
||||
@PostMapping("/seize")
|
||||
public void seizeCoupon(@RequestBody SeizeCouponReqDTO seizeCouponReqDTO) {
|
||||
activityService.seizeCoupon(seizeCouponReqDTO);
|
||||
}
|
||||
}
|
||||
@ -24,6 +24,6 @@ public class CouponController {
|
||||
|
||||
@GetMapping("/page")
|
||||
public PageResult<CouponPageInfoResDTO> pageCoupon(CouponPageQueryDTO couponPageQueryDTO) {
|
||||
return couponService.page(couponPageQueryDTO);
|
||||
return couponService.page(couponPageQueryDTO);
|
||||
}
|
||||
}
|
||||
@ -14,6 +14,7 @@ import com.jzo2o.market.service.IActivityService;
|
||||
import com.jzo2o.market.service.ICouponService;
|
||||
import com.xxl.job.core.handler.annotation.XxlJob;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.data.redis.core.HashOperations;
|
||||
import org.springframework.data.redis.core.RedisTemplate;
|
||||
import org.springframework.stereotype.Component;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
@ -133,12 +134,26 @@ public class XxlJobHandler {
|
||||
List<SeizeCouponInfoResDTO> couponInfoList = activityList.stream()
|
||||
.map(activity -> BeanUtils
|
||||
.toBean(activity, SeizeCouponInfoResDTO.class)
|
||||
.setRemainNum(activity.getStockNum()))
|
||||
.setRemainNum(activity.getStockNum())
|
||||
.setType(activity.getType().getType()))
|
||||
.collect(Collectors.toList());
|
||||
String couponInfoListJsonStr = JsonUtils.toJsonStr(couponInfoList);
|
||||
|
||||
// 缓存活动列表
|
||||
redisTemplate
|
||||
.opsForValue()
|
||||
.set(RedisConstants.RedisKey.ACTIVITY_CACHE_LIST, couponInfoListJsonStr);
|
||||
|
||||
// 缓存优惠卷库存
|
||||
HashOperations<String, Object, Object> hashOperations = redisTemplate.opsForHash();
|
||||
couponInfoList.forEach(coupon -> {
|
||||
String key = String.format(RedisConstants.RedisKey.COUPON_RESOURCE_STOCK, coupon.getId() % 10);
|
||||
// 防止进行中的活动的库存被覆盖
|
||||
if (coupon.getStatus() == ActivityStatusEnum.NO_DISTRIBUTE) {
|
||||
hashOperations.put(key, coupon.getId(), coupon.getRemainNum());
|
||||
} else {
|
||||
hashOperations.putIfAbsent(key, coupon.getId(), coupon.getRemainNum());
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
@ -6,6 +6,7 @@ import com.jzo2o.market.enums.ActivityStatusEnum;
|
||||
import com.jzo2o.market.model.domain.Activity;
|
||||
import com.jzo2o.market.model.dto.request.ActivityPageQueryDTO;
|
||||
import com.jzo2o.market.model.dto.request.ActivitySaveReqDTO;
|
||||
import com.jzo2o.market.model.dto.request.SeizeCouponReqDTO;
|
||||
import com.jzo2o.market.model.dto.response.ActivityInfoResDTO;
|
||||
import com.jzo2o.market.model.dto.response.SeizeCouponInfoResDTO;
|
||||
|
||||
@ -44,4 +45,9 @@ public interface IActivityService extends IService<Activity> {
|
||||
* 撤销活动
|
||||
*/
|
||||
void revoke(Long id);
|
||||
|
||||
/**
|
||||
* 用户端进行抢卷操作
|
||||
*/
|
||||
void seizeCoupon(SeizeCouponReqDTO seizeCouponReqDTO);
|
||||
}
|
||||
@ -6,13 +6,14 @@ import com.baomidou.mybatisplus.core.toolkit.Wrappers;
|
||||
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
|
||||
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
|
||||
import com.baomidou.mybatisplus.extension.toolkit.ChainWrappers;
|
||||
import com.jzo2o.common.constants.ErrorInfo;
|
||||
import com.jzo2o.common.expcetions.CommonException;
|
||||
import com.jzo2o.common.expcetions.DBException;
|
||||
import com.jzo2o.common.expcetions.ForbiddenOperationException;
|
||||
import com.jzo2o.common.model.PageResult;
|
||||
import com.jzo2o.common.utils.BeanUtils;
|
||||
import com.jzo2o.common.utils.CollUtils;
|
||||
import com.jzo2o.common.utils.JsonUtils;
|
||||
import com.jzo2o.market.constants.RedisConstants;
|
||||
import com.jzo2o.market.enums.ActivityStatusEnum;
|
||||
import com.jzo2o.market.enums.CouponStatusEnum;
|
||||
import com.jzo2o.market.enums.CouponTypeEnum;
|
||||
@ -21,21 +22,29 @@ import com.jzo2o.market.model.domain.Activity;
|
||||
import com.jzo2o.market.model.domain.Coupon;
|
||||
import com.jzo2o.market.model.dto.request.ActivityPageQueryDTO;
|
||||
import com.jzo2o.market.model.dto.request.ActivitySaveReqDTO;
|
||||
import com.jzo2o.market.model.dto.request.SeizeCouponReqDTO;
|
||||
import com.jzo2o.market.model.dto.response.ActivityInfoResDTO;
|
||||
import com.jzo2o.market.model.dto.response.SeizeCouponInfoResDTO;
|
||||
import com.jzo2o.market.service.IActivityService;
|
||||
import com.jzo2o.market.service.ICouponService;
|
||||
import com.jzo2o.mvc.utils.UserContext;
|
||||
import com.jzo2o.mysql.utils.PageUtils;
|
||||
import com.jzo2o.redis.utils.RedisSyncQueueUtils;
|
||||
import org.springframework.data.redis.core.RedisTemplate;
|
||||
import org.springframework.data.redis.core.script.RedisScript;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
import javax.annotation.Resource;
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import static com.jzo2o.market.constants.RedisConstants.RedisKey.*;
|
||||
|
||||
/**
|
||||
* <p>
|
||||
* 服务实现类
|
||||
@ -49,6 +58,10 @@ public class ActivityServiceImpl extends ServiceImpl<ActivityMapper, Activity> i
|
||||
private ICouponService couponService;
|
||||
@Resource
|
||||
private RedisTemplate<String, Object> redisTemplate;
|
||||
@Resource(name = "seizeCouponScript")
|
||||
private RedisScript<Integer> seizeCouponScript;
|
||||
@Resource
|
||||
private RedisSyncProperties redisSyncProperties;
|
||||
|
||||
@Override
|
||||
@SuppressWarnings("unchecked")
|
||||
@ -92,7 +105,7 @@ public class ActivityServiceImpl extends ServiceImpl<ActivityMapper, Activity> i
|
||||
@Override
|
||||
public List<SeizeCouponInfoResDTO> getCachedActivity(ActivityStatusEnum status) {
|
||||
List<SeizeCouponInfoResDTO> couponInfoList = JsonUtils.toList((String) redisTemplate.opsForValue()
|
||||
.get(RedisConstants.RedisKey.ACTIVITY_CACHE_LIST), SeizeCouponInfoResDTO.class);
|
||||
.get(ACTIVITY_CACHE_LIST), SeizeCouponInfoResDTO.class);
|
||||
|
||||
if (CollUtils.isEmpty(couponInfoList)) {
|
||||
return new ArrayList<>();
|
||||
@ -164,4 +177,44 @@ public class ActivityServiceImpl extends ServiceImpl<ActivityMapper, Activity> i
|
||||
.set(Coupon::getStatus, CouponStatusEnum.VOIDED)
|
||||
.update();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void seizeCoupon(SeizeCouponReqDTO seizeCouponReqDTO) {
|
||||
Long activityId = seizeCouponReqDTO.getId();
|
||||
Activity activity = lambdaQuery()
|
||||
.eq(Activity::getId, activityId)
|
||||
.select(Activity::getDistributeStartTime, Activity::getDistributeEndTime)
|
||||
.one();
|
||||
|
||||
int seizeCouponFailedCode = ErrorInfo.Code.SEIZE_COUPON_FAILD;
|
||||
if (ObjectUtils.isEmpty(activity)) {
|
||||
throw new CommonException(seizeCouponFailedCode, "活动不存在无法抢卷");
|
||||
} else if (activity.getDistributeStartTime().isAfter(LocalDateTime.now())) {
|
||||
throw new CommonException(seizeCouponFailedCode, "活动未开始无法抢卷");
|
||||
} else if (activity.getDistributeEndTime().isBefore(LocalDateTime.now())) {
|
||||
throw new CommonException(seizeCouponFailedCode, "活动已结束无法抢卷");
|
||||
}
|
||||
|
||||
Long userId = UserContext.currentUserId();
|
||||
if (ObjectUtils.isEmpty(userId)) {
|
||||
throw new CommonException(seizeCouponFailedCode, "用户信息不存在无法抢卷");
|
||||
}
|
||||
|
||||
int activityTag = (int) (activityId % redisSyncProperties.getQueueNum());
|
||||
String syncQueueKey = RedisSyncQueueUtils.getQueueRedisKey(COUPON_SEIZE_SYNC_QUEUE_NAME, activityTag);
|
||||
String stockTable = String.format(COUPON_RESOURCE_STOCK, activityTag);
|
||||
String seizeSuccessList = String.format(COUPON_SEIZE_LIST, activityId, activityTag);
|
||||
List<String> keys = Arrays.asList(syncQueueKey, stockTable, seizeSuccessList);
|
||||
|
||||
// 调用抢卷脚本
|
||||
int state = Optional
|
||||
.ofNullable(redisTemplate.execute(seizeCouponScript, keys, activityId, userId))
|
||||
.orElseThrow(() -> new CommonException(seizeCouponFailedCode, "抢卷失败"));
|
||||
|
||||
if (state < 0) {
|
||||
String failMag = state == -1 ? "请勿重复抢卷" :
|
||||
(state == -2 || state == -4 ? "库存不足抢卷失败" : "内部错误抢卷失败");
|
||||
throw new CommonException(seizeCouponFailedCode, failMag);
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1,16 +0,0 @@
|
||||
package com.jzo2o.market.utils;
|
||||
|
||||
import cn.hutool.core.util.ObjectUtil;
|
||||
import com.jzo2o.market.enums.ActivityTypeEnum;
|
||||
import com.jzo2o.market.model.domain.Coupon;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.math.RoundingMode;
|
||||
|
||||
/**
|
||||
* 优惠券相关工具
|
||||
*/
|
||||
public class CouponUtils {
|
||||
|
||||
|
||||
}
|
||||
@ -1,37 +1,36 @@
|
||||
-- 抢券lua实现
|
||||
-- key: 抢券同步队列,资源库存,抢券成功列表
|
||||
-- argv:活动id,用户id
|
||||
-- key: 抢券同步队列, 资源库存, 抢券成功列表
|
||||
-- argv:活动id, 用户id
|
||||
|
||||
--优惠券是否已经抢过
|
||||
-- 优惠券是否已经抢过
|
||||
local couponNum = redis.call("HGET", KEYS[3], ARGV[2])
|
||||
-- hget 获取不到数据返回false而不是nil
|
||||
if couponNum ~= false and tonumber(couponNum) >= 1
|
||||
then
|
||||
return "-1";
|
||||
return "-1"; -- 已抢卷
|
||||
end
|
||||
-- --库存是否充足校验
|
||||
local stockNum = redis.call("HGET",KEYS[2], ARGV[1])
|
||||
if stockNum == false or tonumber(stockNum) < 1
|
||||
-- 库存是否充足校验
|
||||
local stockNum = redis.call("HGET", KEYS[2], ARGV[1])
|
||||
if stockNum == false or tonumber(stockNum) < 1
|
||||
then
|
||||
return "-2";
|
||||
return "-2"; -- 库存不足抢卷失败
|
||||
end
|
||||
--抢券列表
|
||||
local listNum = redis.call("HSET",KEYS[3], ARGV[2], 1)
|
||||
if listNum == false or tonumber(listNum) < 1
|
||||
-- 抢券成功列表
|
||||
local listNum = redis.call("HSET", KEYS[3], ARGV[2], 1)
|
||||
if listNum == false or tonumber(listNum) < 1
|
||||
then
|
||||
return "-3";
|
||||
return "-3"; -- 写入抢卷成功列表失败
|
||||
end
|
||||
|
||||
--减库存
|
||||
stockNum = redis.call("HINCRBY",KEYS[2], ARGV[1], -1)
|
||||
-- 减少库存
|
||||
stockNum = redis.call("HINCRBY", KEYS[2], ARGV[1], -1)
|
||||
if tonumber(stockNum) < 0
|
||||
then
|
||||
return "-4"
|
||||
return "-4" -- 库存不足抢卷失败
|
||||
end
|
||||
-- 抢单结果写入同步队列
|
||||
local result = redis.call("HSETNX", KEYS[1], ARGV[2],ARGV[1])
|
||||
-- 抢卷结果写入同步队列
|
||||
local result = redis.call("HSETNX", KEYS[1], ARGV[2], ARGV[1])
|
||||
if result > 0
|
||||
then
|
||||
return ARGV[1] ..""
|
||||
return "100" -- 抢卷成功返回活动id
|
||||
end
|
||||
return "-5"
|
||||
return "-5" -- 写入同步队列失败
|
||||
Loading…
x
Reference in New Issue
Block a user