我们拆掉了项目管理工具里的 “墙”
2025/12/17 22:10:55
| 接口说明 | 查询发放中的优惠券 |
|---|---|
| 请求方式 | GET |
| 请求路径 | /coupons/list |
| 请求参数 | 无 |
| 返回值 | [ { "id": "110", // 优惠券id "name": "年中大促", // 优惠券名称 "specific": true, // 优惠券是否限定了课程范围 "discountType": "", // 折扣类型 "thresholdAmount": 0 // 折扣门槛 "discountValue": 0, // 折扣值 "maxDiscountAmount": 0, // 最大折扣金额 "termDays": 0, // 有效天数 "termEndTime": "", // 过期时间 "available": true, // 是否可领取 "received": true, // 是否已领取 } ] |
/** * 查询发放中的优惠券列表 * @return */@ApiOperation("查询发放中的优惠券列表")@GetMapping("list")publicList<CouponVO>queryIssuingCoupons(){returncouponService.queryIssuingCoupons();}List<CouponVO>queryIssuingCoupons();@OverridepublicList<CouponVO>queryIssuingCoupons(){//1.查询属于手动领取以及发放中的优惠券List<Coupon>list=lambdaQuery().eq(Coupon::getStatus,CouponStatus.ISSUING).eq(Coupon::getObtainWay,PUBLIC).list();if(CollUtils.isEmpty(list)){returnCollUtils.emptyList();}List<Long>ids=list.stream().map(Coupon::getId).collect(Collectors.toList());// 2.查询用户领取的并符合条件的优惠券List<UserCoupon>eq=userCouponService.lambdaQuery().eq(UserCoupon::getUserId,UserContext.getUser()).in(UserCoupon::getCouponId,ids).list();//2.1当前用户已经领取的数量Map<Long,Long>map=eq.stream().collect(Collectors.groupingBy(UserCoupon::getCouponId,Collectors.counting()));//2.2当前用户对优惠券已经领取但是没使用的数量Map<Long,Long>unused=eq.stream().filter(uc->uc.getStatus().equals(UserCouponStatus.UNUSED)).collect(Collectors.groupingBy(UserCoupon::getCouponId,Collectors.counting()));//3.封装优惠券信息并返回ArrayList<CouponVO>couponVOS=newArrayList<>();for(Couponcoupon:list){CouponVOcouponVO=BeanUtils.copyBean(coupon,CouponVO.class);//3.是否可以领取(被领取数量未达到总发放数量,当前用户领取数量小于每人最多领取数量)couponVO.setAvailable(coupon.getIssueNum()<coupon.getTotalNum()&&map.getOrDefault(coupon.getId(),0L)<coupon.getUserLimit());//4.是否可以使用(未使用的)couponVO.setReceived(unused.getOrDefault(coupon.getId(),0L)>0);couponVOS.add(couponVO);}returncouponVOS;}/** * 领取优惠券(方式为手动领取的优惠券) * * @param couponId * @return */@PostMapping("{couponId}/receive")@ApiOperation("领取优惠券")publicvoidreceiveCoupon(@PathVariableLongcouponId){userCouponService.receiveCoupon(couponId);}voidreceiveCoupon(LongcouponId);@Override@TransactionalpublicvoidreceiveCoupon(LongcouponId){Couponcoupon=couponMapper.selectById(couponId);if(coupon==null){thrownewBizIllegalException("优惠券不存在");}LocalDateTimenow=LocalDateTime.now();if(now.isBefore(coupon.getIssueBeginTime())||now.isAfter(coupon.getIssueEndTime())){thrownewBizIllegalException("优惠券不在领取时间范围内");}LonguserId=UserContext.getUser();Longresult=redisLuaService.tryReceiveCoupon(couponId,userId,coupon.getUserLimit());if(result==null){thrownewBizIllegalException("系统繁忙");}if(result==-1){thrownewBizIllegalException("超过个人领取上限");}if(result==0){thrownewBizIllegalException("库存不足");}try{saveUserCouponWithTx(coupon,userId,now);}catch(Exceptione){redisLuaService.rollbackCoupon(couponId,userId);throwe;}}@TransactionalpublicvoidsaveUserCouponWithTx(Couponcoupon,LonguserId,LocalDateTimenow){// 1. 校验每人限领数量(兜底)Integercount=lambdaQuery().eq(UserCoupon::getUserId,userId).eq(UserCoupon::getCouponId,coupon.getId()).count();if(count!=null&&count>=coupon.getUserLimit()){thrownewBizIllegalException("该用户领取数量超出限制");}// 2. 乐观更新优惠券发放数量(最终防线)introws=couponMapper.incrIssueNumWithLimit(coupon.getId());if(rows==0){thrownewBizIllegalException("优惠券库存不足");}// 3. 新增用户优惠券addCoupon(coupon.getId(),coupon,now,userId);}privatevoidaddCoupon(LongcouponId,Couponcoupon,LocalDateTimenow,LonguserId){UserCouponuserCoupon=newUserCoupon();LocalDateTimetermBeginTime=coupon.getTermBeginTime();LocalDateTimetermEndTime=coupon.getTermEndTime();if(termBeginTime==null){termBeginTime=now;termEndTime=termBeginTime.plusDays(coupon.getTermDays());}userCoupon.setUserId(userId);userCoupon.setCouponId(couponId);userCoupon.setTermBeginTime(termBeginTime);userCoupon.setTermEndTime(termEndTime);userCoupon.setStatus(UserCouponStatus.UNUSED);this.save(userCoupon);}-- KEYS[1] = coupon:stock:{couponId}-- KEYS[2] = coupon:user:{couponId}-- ARGV[1] = userId-- ARGV[2] = userLimit-- 1. 查询用户已领取数量localcount=tonumber(redis.call('HGET',KEYS[2],ARGV[1])or"0")locallimit=tonumber(ARGV[2])ifcount>=limitthenreturn-1-- 超过个人限领end-- 2. 校验库存localstock=tonumber(redis.call('GET',KEYS[1]))ifnotstockorstock<=0thenreturn0-- 库存不足end-- 3. 扣库存redis.call('DECR',KEYS[1])-- 4. 用户领取数量 +1redis.call('HINCRBY',KEYS[2],ARGV[1],1)-- 5. 成功return1packagecom.tianji.promotion.config;importcom.tianji.promotion.constants.PromotionConstants;importlombok.RequiredArgsConstructor;importorg.springframework.core.io.ClassPathResource;importorg.springframework.data.redis.core.RedisTemplate;importorg.springframework.data.redis.core.StringRedisTemplate;importorg.springframework.data.redis.core.script.DefaultRedisScript;importorg.springframework.stereotype.Service;importjava.util.Arrays;importjava.util.List;/** * Redis Lua 执行统一封装 * * 职责: * 1. 负责 Lua 脚本加载 * 2. 统一管理 Redis Key 拼装 * 3. 对业务层屏蔽 Lua 细节 * @author ABC */@Service@RequiredArgsConstructorpublicclassRedisLuaService{privatefinalStringRedisTemplateredisTemplate;privatestaticfinalDefaultRedisScript<Long>RECEIVE_COUPON_SCRIPT;static{RECEIVE_COUPON_SCRIPT=newDefaultRedisScript<>();RECEIVE_COUPON_SCRIPT.setLocation(newClassPathResource("redis/lua/receive_coupon.lua"));RECEIVE_COUPON_SCRIPT.setResultType(Long.class);}publicLongtryReceiveCoupon(LongcouponId,LonguserId,IntegeruserLimit){StringstockKey=PromotionConstants.COUPON_STOCK_KEY+couponId;StringuserCountKey=PromotionConstants.COUPON_USER_COUNT_KEY+couponId;returnredisTemplate.execute(RECEIVE_COUPON_SCRIPT,List.of(stockKey,userCountKey),userId.toString(),userLimit.toString());}/** * DB 失败回滚 */publicvoidrollbackCoupon(LongcouponId,LonguserId){StringstockKey=PromotionConstants.COUPON_STOCK_KEY+couponId;StringuserCountKey=PromotionConstants.COUPON_USER_COUNT_KEY+couponId;redisTemplate.opsForValue().increment(stockKey);redisTemplate.opsForHash().increment(userCountKey,userId.toString(),-1);}}packagecom.tianji.promotion.constants;/** * 优惠券常量类 * * @author ax */publicinterfacePromotionConstants{/** * 优惠券的兑换码生成序列号key */StringCOUPON_CODE_SERIAL_KEY="coupon:code:serial:";/** * 优惠券的兑换码兑换序列号key */StringCOUPON_CODE_MAP_KEY="coupon:code:serial:";/** * 优惠券库存 * coupon:stock:{couponId} -> int */StringCOUPON_STOCK_KEY="coupon:stock:";/** * 用户已领取数量 * coupon:user:{couponId} -> Hash(userId -> count) */StringCOUPON_USER_COUNT_KEY="coupon:user:";}@Update("update coupon set issue_num = issue_num + 1 where id = #{couponId} and issue_num < total_num")intincrIssueNumWithLimit(LongcouponId);/** * 兑换码兑换优惠券(方式为兑换码兑换的优惠券) * * @return */@PostMapping("{code}/exchange")@ApiOperation("兑换码兑换优惠券")publicvoidexchangeCoupon(@PathVariableStringcode){userCouponService.exchangeCoupon(code);}voidexchangeCoupon(Stringcode);@Override@TransactionalpublicvoidexchangeCoupon(Stringcode){//校验兑换码(是否被兑换过,是否存在)longl=CodeUtil.parseCode(code);//是否已经兑换过 setbit替换getbitbooleanisExchange=exchangeCodeService.updateExchangeMark(l,true);try{if(isExchange){thrownewBizIllegalException("该兑换码已经兑换过");}ExchangeCodebyId=exchangeCodeService.getById(l);if(byId==null){thrownewBizIllegalException("该兑换码不存在");}LocalDateTimenow=LocalDateTime.now();if(now.isAfter(byId.getExpiredTime())){thrownewBizIllegalException("该兑换码已过期");}//查询优惠券Couponcoupon=couponMapper.selectById(byId.getExchangeTargetId());LonguserId=UserContext.getUser();//领取优惠券saveUserCouponWithTx(coupon,userId,now);//更新兑换码状态exchangeCodeService.lambdaUpdate().eq(ExchangeCode::getId,l).set(ExchangeCode::getUserId,userId).set(ExchangeCode::getStatus,ExchangeCodeStatus.USED).update();}catch(Exceptione){exchangeCodeService.updateExchangeMark(l,false);throwe;}}booleanupdateExchangeMark(longl,booleanb);@OverridepublicbooleanupdateExchangeMark(longl,booleanb){Booleanis=stringRedisTemplate.opsForValue().setBit(COUPON_CODE_MAP_KEY,l,b);returnis!=null&&is;}@Update("update coupon set issue_num = issue_num + 1 where id = #{couponId} and issue_num < total_num")intincrIssueNumWithLimit(LongcouponId);我们可以借助AspectJ来实现。
1)引入AspectJ依赖:
<!--aspecj--><dependency><groupId>org.aspectj</groupId><artifactId>aspectjweaver</artifactId></dependency>2)暴露代理对象
在启动类上添加注解,暴露代理对象:
3)使用代理对象
最后,改造领取优惠券的代码,获取代理对象来调用事务方法: