news 2026/5/26 20:48:33

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

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
天机学堂-优惠券领取功能-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)使用代理对象

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

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/5/26 7:14:55

定制报告-个性化定制-按需专项研究报告-智信中科研究网

定制报告-个性化定制-按需专项研究报告-智信中科研究网市场专项研究报告智信中科研究网个性化定制报告全力解决您的各类需求&#xff0c;为了满足客户在不同发展阶段的不同需求&#xff0c;智信中科研究网可以依据客户的个性化需求&#xff0c;针对性帮助客户完成符合需求的市场…

作者头像 李华
网站建设 2026/5/26 6:49:57

Linux 内存管理:TLB ASID

文章目录1. 前言2. TLB ASID 的硬件支持2.1 概念2.2 TLB 查找3. Linux 下 TLB ASID 管理4. 参考资料1. 前言 限于作者能力水平&#xff0c;本文可能存在谬误&#xff0c;因此而给读者带来的损失&#xff0c;作者不做任何承诺。 2. TLB ASID 的硬件支持 2.1 概念 什么是 TLB&…

作者头像 李华
网站建设 2026/5/26 5:48:49

基于微信小程序的数字博物馆系统的设计与实现(程序+文档+讲解)

课题介绍在文博资源数字化、展览体验便捷化需求升级的背景下&#xff0c;传统博物馆存在 “线下参观受限、展品解读单一、互动体验不足” 的痛点&#xff0c;基于微信小程序 SpringBoot 构建的数字博物馆系统&#xff0c;适配游客、博物馆管理员、讲解员等角色&#xff0c;实现…

作者头像 李华
网站建设 2026/5/25 13:17:03

代码随想录算法训练营Day49 | Prim算法、Kruskal算法

Prim算法 53. 寻宝&#xff08;第七期模拟笔试&#xff09; 1.思路 本题是最小生成树的模板题&#xff0c;图中有n个节点&#xff0c;那么一定可以用 n-1 条边将所有节点连接到一起&#xff0c;并且总权重最小。 Prim 算法&#xff1a;从一个顶点开始&#xff0c;逐步“生长”…

作者头像 李华
网站建设 2026/5/25 18:28:05

30、Linux软件包管理与源码构建全攻略

Linux软件包管理与源码构建全攻略 在Linux系统中,软件包的管理和源码构建是非常重要的操作。下面将详细介绍DEB文件处理、源码构建以及SRPM安装的相关内容。 1. RPM验证报告字符含义 在RPM验证报告中,有一些特定的字符代表着不同的含义,如下表所示: | 字符 | 含义 | |…

作者头像 李华
网站建设 2026/5/26 5:49:58

Flink Rebalance、Rescale、Shuffle 核心区别

Flink 中的 Rebalance、Rescale、Shuffle 是三种核心的数据流分区策略,用于定义上游算子的并行子任务如何将数据分发到下游算子的并行子任务。三者的核心差异体现在数据分配方式、网络开销、适用场景上,选择不当会直接影响作业的性能和数据分布的均匀性。以下是详细的对比与解…

作者头像 李华