当前位置: 首页 > news >正文

天机学堂-优惠券领取功能-day10(八)

day10接口

1 查询发放中的优惠券

接口说明查询发放中的优惠券
请求方式GET
请求路径/coupons/list
请求参数
返回值[ { "id": "110", // 优惠券id "name": "年中大促", // 优惠券名称 "specific": true, // 优惠券是否限定了课程范围 "discountType": "", // 折扣类型 "thresholdAmount": 0 // 折扣门槛 "discountValue": 0, // 折扣值 "maxDiscountAmount": 0, // 最大折扣金额 "termDays": 0, // 有效天数 "termEndTime": "", // 过期时间 "available": true, // 是否可领取 "received": true, // 是否已领取 } ]
CouponController.java
/** * 查询发放中的优惠券列表 * @return */@ApiOperation("查询发放中的优惠券列表")@GetMapping("list")publicList<CouponVO>queryIssuingCoupons(){returncouponService.queryIssuingCoupons();}
ICouponService.java
List<CouponVO>queryIssuingCoupons();
CouponServiceImpl.java
@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;}

2 手动领取优惠券

UserCouponController.java
/** * 领取优惠券(方式为手动领取的优惠券) * * @param couponId * @return */@PostMapping("{couponId}/receive")@ApiOperation("领取优惠券")publicvoidreceiveCoupon(@PathVariableLongcouponId){userCouponService.receiveCoupon(couponId);}
IUserCouponService.java
voidreceiveCoupon(LongcouponId);
UserCouponServiceImpl.java
@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);}
Lua脚本

-- 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. 成功return1
LUA配置类
packagecom.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);}}
Redis常量Key
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:";}
CouponMapper.java
@Update("update coupon set issue_num = issue_num + 1 where id = #{couponId} and issue_num < total_num")intincrIssueNumWithLimit(LongcouponId);

3 兑换码兑换优惠券

UserCouponController.java
/** * 兑换码兑换优惠券(方式为兑换码兑换的优惠券) * * @return */@PostMapping("{code}/exchange")@ApiOperation("兑换码兑换优惠券")publicvoidexchangeCoupon(@PathVariableStringcode){userCouponService.exchangeCoupon(code);}
IUserCouponService.java
voidexchangeCoupon(Stringcode);
UserCouponServiceImpl.java
@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;}}
IExchangeCodeService
booleanupdateExchangeMark(longl,booleanb);
ExchangeCodeServiceImpl
@OverridepublicbooleanupdateExchangeMark(longl,booleanb){Booleanis=stringRedisTemplate.opsForValue().setBit(COUPON_CODE_MAP_KEY,l,b);returnis!=null&&is;}
CouponMapper.java
@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)使用代理对象

最后,改造领取优惠券的代码,获取代理对象来调用事务方法:

http://www.cnnetsun.cn/news/113965.html

相关文章:

  • 2025 OA 选型关键看这 4 点:集成、灵活、安全、易用,附高性价比系统清单
  • 图神经网络与pytorch
  • Xiaomi 商城页面布局(部分)
  • FPGA以太网升级程序:便捷qspi Flash升级,具备校验功能,适用于Xilinx 7系列...
  • 运料小车装卸料控制:西门子1200PLC与TP700触摸屏联机仿真博途16
  • S32K311启动过程中,向量表重定向
  • 从蓝图到产线:高效产品信息传递的桥梁建设
  • 时间复杂度
  • 网站建设公司怎么选?2025年网站设计制作公司推荐指南
  • 今天咱们来聊一个挺有意思的优化算法改进——基于透镜成像反向策略的海洋捕食者算法。这个改进版本在原始MPA基础上搞了点新花样,咱们直接上干货看代码实现
  • Gitee:本土化DevOps平台如何重塑中国开发者生态
  • vCenter Server 8.0U3h 新增功能简介
  • Cisco NX-OS 10.6(2)F 发布 - 数据中心网络操作系统
  • Ubuntu24.04无操作卡死,无法唤醒问题以及内核版本切换记录
  • 全场景覆盖・全流程智控:分布式解决方案让多功能厅 “不止于多”
  • 【轨物方案】聚焦锯床设备智能化升级,打造工业互联网新范式
  • 【轨物交流】轨物科技亮相2025高校科技成果交易会
  • cesium加载geotiff的 四种方法
  • 【毕业设计】基于python的运维管理平台的设计与实现
  • 苹果 iOS 开发真正复杂的不是写代码这方面,是证书、构建、上架
  • FSMC-TFTLCD显示实验(5):显示一个字符串的函数传递过程追踪~
  • 基于Android的课程考勤及作业提交系统
  • 飞易通蓝牙与Wi-Fi模块:医疗产品无线连接的全能助手
  • 你的音效素材库该升级了!这个网站的分类细到超出你想象
  • Agent的“话痨”病有救了!微软黑科技教你压缩对话历史,让AI告别失忆,这篇教程太顶了!
  • ARMv7 linux中断路由以及处理
  • 【详解】基于Kubernetes部署Kafka集群
  • AIoT:从万物互联到万物智联的进化之路
  • ERROR in ./node_modules/vue-router/dist/vue-router.mjs 被报错折磨半天?真相竟是……
  • Spring Boot 自动配置的底层实现原理