news 2026/4/21 23:33:36

天机学堂-优惠券领取功能-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/4/18 11:01:24

EmotiVoice在心理治疗暴露疗法中的辅助作用

EmotiVoice在心理治疗暴露疗法中的辅助作用 在创伤后应激障碍&#xff08;PTSD&#xff09;或特定恐惧症的临床干预中&#xff0c;一个核心挑战是如何安全、可控地引导患者重新面对那些引发强烈焦虑的记忆场景。传统暴露疗法依赖治疗师口头描述情境&#xff0c;或播放预先录制的…

作者头像 李华
网站建设 2026/4/18 9:07:57

为什么工作10年都没遇过分布式锁?

引言&#xff1a;分布式锁的「薛定谔状态」在2025年某次技术峰会上&#xff0c;某互联网大厂架构师分享的监控截图引发热议&#xff1a;核心交易系统QPS峰值突破50万&#xff0c;但分布式锁调用次数为零。这印证了行业中的一个普遍现象——多数开发者终其职业生涯都未真正接触过…

作者头像 李华
网站建设 2026/4/17 14:25:28

冻品小程序开发选哪家?首选万象生鲜配送系统

在生鲜冻品小程序开发领域&#xff0c;选择适配冷链管理、库存精准、履约高效的系统是核心。万象生鲜系统凭借对冻品行业的深度适配、全链路温控与数字化管理能力&#xff0c;成为冻品商家开发小程序的优选方案&#xff0c;下面从行业痛点、系统优势、对比分析等方面详细解析。…

作者头像 李华
网站建设 2026/4/18 1:34:13

langchain agent动态变更系统prompt

一.背景 LangChain Agent 作为大语言模型&#xff08;LLM&#xff09;驱动的智能体核心&#xff0c;** 系统 Prompt&#xff08;提示词&#xff09;** 是其行为准则与决策逻辑的 “顶层设计”—— 它定义了 Agent 的角色定位&#xff08;如 “智能客服”“数据分析师”&#xf…

作者头像 李华
网站建设 2026/4/18 3:35:32

springboot+jspm电力监测报修服务平台_34gate3m

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

作者头像 李华
网站建设 2026/4/17 7:57:12

基于PSO - BP的时间序列预测:一键出图的奇妙之旅

基于粒子群优化算法优化BP神经网络(PSO-BP)的时间序列预测 PSO-BP时间序列 一键出图&#xff0c;只需替换成自己数据集即可&#xff0c;有教程。在数据的海洋中&#xff0c;时间序列预测一直是众多领域关注的焦点。今天咱就唠唠基于粒子群优化算法&#xff08;PSO&#xff09;优…

作者头像 李华