From c09e6bf68089c75aab80e12dad73b824279f9aa4 Mon Sep 17 00:00:00 2001 From: JIAN Date: Thu, 26 Sep 2024 20:00:52 +0800 Subject: [PATCH] =?UTF-8?q?feat(market):=E6=96=B0=E5=A2=9E=E7=BC=93?= =?UTF-8?q?=E5=AD=98=E5=BA=93=E5=AD=98=E5=92=8C=E7=94=A8=E6=88=B7=E7=AB=AF?= =?UTF-8?q?=E6=8A=A2=E5=8D=B7=E7=9A=84=E5=8A=9F=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../market/config/RedisLuaConfiguration.java | 28 ++------- .../controller/consumer/CouponController.java | 24 ++++++-- .../operation/CouponController.java | 2 +- .../jzo2o/market/handler/XxlJobHandler.java | 17 +++++- .../market/service/IActivityService.java | 6 ++ .../service/impl/ActivityServiceImpl.java | 57 ++++++++++++++++++- .../com/jzo2o/market/utils/CouponUtils.java | 16 ------ .../resources/scripts/seizeCouponScript.lua | 39 +++++++------ 8 files changed, 121 insertions(+), 68 deletions(-) delete mode 100644 jzo2o-market/src/main/java/com/jzo2o/market/utils/CouponUtils.java diff --git a/jzo2o-market/src/main/java/com/jzo2o/market/config/RedisLuaConfiguration.java b/jzo2o-market/src/main/java/com/jzo2o/market/config/RedisLuaConfiguration.java index f8991b1..a7b8405 100644 --- a/jzo2o-market/src/main/java/com/jzo2o/market/config/RedisLuaConfiguration.java +++ b/jzo2o-market/src/main/java/com/jzo2o/market/config/RedisLuaConfiguration.java @@ -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 seizeCouponScript() { DefaultRedisScript 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 getLuaTest01() { - DefaultRedisScript redisScript = new DefaultRedisScript<>(); - //resource目录下的scripts文件下的lua_test01.lua文件 - redisScript.setScriptSource(new ResourceScriptSource(new ClassPathResource("scripts/lua_test01.lua"))); - redisScript.setResultType(Integer.class); - return redisScript; - } - -} +} \ No newline at end of file diff --git a/jzo2o-market/src/main/java/com/jzo2o/market/controller/consumer/CouponController.java b/jzo2o-market/src/main/java/com/jzo2o/market/controller/consumer/CouponController.java index 8e18b7d..b7e5c96 100644 --- a/jzo2o-market/src/main/java/com/jzo2o/market/controller/consumer/CouponController.java +++ b/jzo2o-market/src/main/java/com/jzo2o/market/controller/consumer/CouponController.java @@ -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 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); + } } \ No newline at end of file diff --git a/jzo2o-market/src/main/java/com/jzo2o/market/controller/operation/CouponController.java b/jzo2o-market/src/main/java/com/jzo2o/market/controller/operation/CouponController.java index d9664da..fbde2a6 100644 --- a/jzo2o-market/src/main/java/com/jzo2o/market/controller/operation/CouponController.java +++ b/jzo2o-market/src/main/java/com/jzo2o/market/controller/operation/CouponController.java @@ -24,6 +24,6 @@ public class CouponController { @GetMapping("/page") public PageResult pageCoupon(CouponPageQueryDTO couponPageQueryDTO) { - return couponService.page(couponPageQueryDTO); + return couponService.page(couponPageQueryDTO); } } \ No newline at end of file diff --git a/jzo2o-market/src/main/java/com/jzo2o/market/handler/XxlJobHandler.java b/jzo2o-market/src/main/java/com/jzo2o/market/handler/XxlJobHandler.java index d7dbd5f..fe3a9ac 100644 --- a/jzo2o-market/src/main/java/com/jzo2o/market/handler/XxlJobHandler.java +++ b/jzo2o-market/src/main/java/com/jzo2o/market/handler/XxlJobHandler.java @@ -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 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 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()); + } + }); } } \ No newline at end of file diff --git a/jzo2o-market/src/main/java/com/jzo2o/market/service/IActivityService.java b/jzo2o-market/src/main/java/com/jzo2o/market/service/IActivityService.java index 09bc901..b7d8573 100644 --- a/jzo2o-market/src/main/java/com/jzo2o/market/service/IActivityService.java +++ b/jzo2o-market/src/main/java/com/jzo2o/market/service/IActivityService.java @@ -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 { * 撤销活动 */ void revoke(Long id); + + /** + * 用户端进行抢卷操作 + */ + void seizeCoupon(SeizeCouponReqDTO seizeCouponReqDTO); } \ No newline at end of file diff --git a/jzo2o-market/src/main/java/com/jzo2o/market/service/impl/ActivityServiceImpl.java b/jzo2o-market/src/main/java/com/jzo2o/market/service/impl/ActivityServiceImpl.java index 46dd283..c3aa948 100644 --- a/jzo2o-market/src/main/java/com/jzo2o/market/service/impl/ActivityServiceImpl.java +++ b/jzo2o-market/src/main/java/com/jzo2o/market/service/impl/ActivityServiceImpl.java @@ -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.*; + /** *

* 服务实现类 @@ -49,6 +58,10 @@ public class ActivityServiceImpl extends ServiceImpl i private ICouponService couponService; @Resource private RedisTemplate redisTemplate; + @Resource(name = "seizeCouponScript") + private RedisScript seizeCouponScript; + @Resource + private RedisSyncProperties redisSyncProperties; @Override @SuppressWarnings("unchecked") @@ -92,7 +105,7 @@ public class ActivityServiceImpl extends ServiceImpl i @Override public List getCachedActivity(ActivityStatusEnum status) { List 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 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 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); + } + } } \ No newline at end of file diff --git a/jzo2o-market/src/main/java/com/jzo2o/market/utils/CouponUtils.java b/jzo2o-market/src/main/java/com/jzo2o/market/utils/CouponUtils.java deleted file mode 100644 index ba43c4f..0000000 --- a/jzo2o-market/src/main/java/com/jzo2o/market/utils/CouponUtils.java +++ /dev/null @@ -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 { - - -} diff --git a/jzo2o-market/src/main/resources/scripts/seizeCouponScript.lua b/jzo2o-market/src/main/resources/scripts/seizeCouponScript.lua index a2b755d..43ecfbd 100644 --- a/jzo2o-market/src/main/resources/scripts/seizeCouponScript.lua +++ b/jzo2o-market/src/main/resources/scripts/seizeCouponScript.lua @@ -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" -- 写入同步队列失败 \ No newline at end of file