news 2026/9/2 4:58:38

天机学堂-优惠券领取功能-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/9/2 7:10:48

Android-Audio-为啥不移到packages/module

Audio 为何没有完全迁移到 packages/modules 这是一个非常专业且深刻的问题。Audio 系统的模块化确实是 Android 架构演进中的一个特殊案例。 一、Audio 系统的现状 当前分布 大部分在 frameworks/av/&#xff1a;frameworks/av/ ├── services/audiopolicy/ # 音频策略服…

作者头像 李华
网站建设 2026/9/2 17:13:19

CPT、SFT、DPO分别是什么

在大语言模型&#xff08;LLM, Large Language Model&#xff09;的训练和对齐流程中&#xff0c;CPT、SFT、DPO 是三个关键阶段的缩写&#xff0c;分别代表&#xff1a;1. CPT&#xff1a;Continued Pre-Training&#xff08;继续预训练&#xff09;有时也称为 Domain-specifi…

作者头像 李华
网站建设 2026/9/2 9:49:37

RPA实现企业微信群成员信息抓取的技术难点

一、 引言&#xff08;Introduction&#xff09; 背景&#xff1a; 在企业微信外部群运营中&#xff0c;获取群成员的详细信息&#xff08;如昵称、企业认证状态、职务等&#xff09;是精细化运营的基础。官方API对此类信息的开放程度有限。 RPA的介入&#xff1a; RPA通过模拟…

作者头像 李华
网站建设 2026/9/2 8:28:00

43、Linux 编程:GNU 许可证与入门级 Shell 脚本编写

Linux 编程:GNU 许可证与入门级 Shell 脚本编写 1. Linux 编程中的调试与修复 在 Linux 编程里,调试是一项关键技能。以一个程序因段错误崩溃后的调试为例: (gdb) file dbgtst A program is being debugged already. Kill it? (y or n) y Load new symbol table from …

作者头像 李华
网站建设 2026/9/2 20:16:43

力扣 “两数之和” 最优解:哈希表 O (n) 时间复杂度实现详解

大家好&#xff0c;今天来讲解力扣经典入门题「两数之和」&#xff0c;分享如何用哈希表实现时间复杂度 O (n) 的高效解法。一、题目回顾给定整数数组 nums 和目标值 target&#xff0c;找出数组中和为 target 的两个整数&#xff0c;返回它们的下标。假设输入只有一个答案不能…

作者头像 李华
网站建设 2026/9/2 2:25:38

springboot+jspm高校考研自修室管理系统的设计与实现_g4fduxyz

目录已开发项目效果实现截图开发技术介绍系统开发工具&#xff1a;核心代码参考示例1.建立用户稀疏矩阵&#xff0c;用于用户相似度计算【相似度矩阵】2.计算目标用户与其他用户的相似度系统测试总结源码文档获取/同行可拿货,招校园代理 &#xff1a;文章底部获取博主联系方式&…

作者头像 李华