Java 篇-项目实战-天机学堂(从0到1)-day10

java 篇: 1.基础地基 2.设计原理 3.项目实战


产品原型分析:

然后就是基础代码构架,执行提供 sql 语句,mp 生成代码,当中字段替换成枚举类。过

实现查询发放中的优惠券接口:

代码实现:

复制代码
@Override
public List<CouponVO> queryIssuingCoupons() {
    //1.查询发放中的优惠券列表
    List<Coupon> coupons = lambdaQuery()
            .eq(Coupon::getStatus, CouponStatus._ISSUING_)
            .eq(Coupon::getObtainWay, ObtainType._PUBLIC_)
            .list();

    if (CollUtils._isEmpty_(coupons)){
        return CollUtils._emptyList_();
    }
    //2.统计当前用户已经领取的优惠券的消息
    List<Long> couponIds = coupons.stream().map(Coupon::getId).collect(Collectors._toList_());
    //2.1.查询当前用户已经领取的优惠券的数据
    List<UserCoupon> userCoupons = userCouponService.lambdaQuery()
            .eq(UserCoupon::getUserId, UserContext._getUser_())
            .in(UserCoupon::getCouponId, couponIds)
            .list();
    //2.2.统计当前用户对优惠券的已经领取数量
    Map<Long, Long> issuedMap = userCoupons.stream()
            .collect(Collectors._groupingBy_(UserCoupon::getCouponId, Collectors._counting_()));
    //2.3.统计当前用户对优惠券的已经领取并且未使用的数量
    Map<Long, Long> unusedMap = userCoupons.stream()
            .filter(uc -> uc.getStatus() == UserCouponStatus._UNUSED_)
            .collect(Collectors._groupingBy_(UserCoupon::getCouponId, Collectors._counting_()));
    //3.封装VO结果
    List<CouponVO> list = new ArrayList<>(coupons.size());
    for (Coupon c : coupons){
        //3.1.拷贝PO属性到VO
        CouponVO vo = BeanUtils._copyBean_(c, CouponVO.class);
        list.add(vo);
        //3.2.是否可以领取:已经被领取的数量 < 优惠券总数量 && 当前用户已经领取的数量 < 每人限领数量
        vo.setAvailable(
                c.getIssueNum() < c.getTotalNum()
        && issuedMap.getOrDefault(c.getId(), 0L) < c.getUserLimit());
        //3.3.是否可以使用:当前用户已经领取并且未使用的优惠券数量 > 0
        vo.setReceived(unusedMap.getOrDefault(c.getId(), 0L) > 0);
    }
    return list;
}

这里注意下,然后业务逻辑上注意一下,尽量减少查表的次数。crud 懂

测试:

解决登录拦截放行问题:

解决未登录,不能查看优惠券的问题

回忆一下登录用户获取的整个流程

UserInfoInterceptor:看下有木有用户信息,如果没有就算了,如果有,帮你转为 Long 类型。

LoginAuthInterceptor:登录拦截,根据有木有用户信息,进行判断

那这个登录拦截,拦截哪些路径呢,这就需要去看配置

ResourceInterceptorConfiguration

添加框里这句,就好了

测试一下

成功

实现领取优惠券接口:

代码实现:

复制代码
package com.tianji.promotion.service.impl;

import com.tianji.common.exceptions.BadRequestException;
import com.tianji.common.utils.UserContext;
import com.tianji.promotion.domain.po.Coupon;
import com.tianji.promotion.domain.po.UserCoupon;
import com.tianji.promotion.mapper.CouponMapper;
import com.tianji.promotion.mapper.UserCouponMapper;
import com.tianji.promotion.service.IUserCouponService;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import java.time.LocalDateTime;

_/**_
_ * <p>_
_ * 用户领取优惠券的记录,是真正使用的优惠券信息 服务实现类_
_ * </p>_
_ *_
_ * @author 叶小鸡_
_ * @since 2026-05-02_
_ */_
@Service
@RequiredArgsConstructor
public class UserCouponServiceImpl extends ServiceImpl<UserCouponMapper, UserCoupon> implements IUserCouponService {
    private final CouponMapper couponMapper;
    @Override
    @Transactional
    public void receiveCoupon(Long couponId) {
        //1.查询优惠券
        Coupon coupon = couponMapper.selectById(couponId);
        if (coupon == null){
            throw new BadRequestException("优惠券不存在");
        }
        //2.校验发放时间
        LocalDateTime now = LocalDateTime._now_();
        if (now.isBefore(coupon.getIssueBeginTime()) || now.isAfter(coupon.getIssueEndTime())){
            throw new BadRequestException("优惠券发放已经结束或尚未开始");
        }
        //3.校验库存
        if (coupon.getIssueNum() >= coupon.getTotalNum()){
            throw new BadRequestException("优惠券库存不足");
        }
        Long userId = UserContext._getUser_();
        //4.校验每人限领数量
        //4.1.统计当前用户对当前优惠券的已经领取的数量
        Integer count = lambdaQuery()
                .eq(UserCoupon::getUserId, userId)
                .eq(UserCoupon::getCouponId, couponId)
                .count();
        //4.2.判断
        if (count != null && count >= coupon.getUserLimit()){
            throw new BadRequestException("领取次数太多");
        }
        //5.更新优惠券的已经发放的数量 + 1
        couponMapper.incrIssueNum(couponId);
        //6.新增一个用户券
        saveUserCoupon(coupon, userId);
    }

    private void saveUserCoupon(Coupon coupon, Long userId) {
        //1.基本信息
        UserCoupon uc = new UserCoupon();
        uc.setUserId(userId);
        uc.setCouponId(coupon.getId());
        //2.有效期信息
        LocalDateTime termBeginTime = coupon.getTermBeginTime();
        LocalDateTime termEndTime = coupon.getTermEndTime();
        if (termBeginTime == null){
            termBeginTime = LocalDateTime._now_();
            termEndTime = termBeginTime.plusDays(coupon.getTermDays());
        }
        uc.setTermBeginTime(termBeginTime);
        uc.setTermEndTime(termEndTime);
        //3.保存
        save(uc);
    }
}

避免循环依赖

保存部分代码有点长,封装成一个函数。

最后别忘了加上事务注解

测试一下:

先把 user_coupon 表清空,然后把 coupon 表的 issue_num 设置为 0

ok,不过这里我自己没测,过。

兑换码兑换优惠券:

先查 redis,如果已兑换,直接在 redis 里就把请求给拦截了,不会再去访问数据库,防止恶意刷请求去增加数据库的压力

代码实现:

先搭个基础框架

这里涉及到并发安全问题,如果多个线程去用 GET KEY 判断,返回都是 0,那 1 个码兑换了 N 个券。

如果 GET 和 SET 分开来,就会出现这样的问题。所以这里直接用 SETBIT 替代 GETBIT,因为 SETBIT 也是会返回旧值的。保证两个操作的原子性。

那如果标记为 1,下面没有执行成功怎么办,那就手动回滚

复制代码
@Override
@Transactional
public void exchangeCoupon(String code) {
    //1.校验并解析兑换码
    long serialNum = CodeUtil._parseCode_(code);
    //2.校验是否已经兑换 GETBIT KEY 4 1
    boolean exchanged = codeService.updateExchangeMark(serialNum, true);
    if (exchanged){
        throw new BizIllegalException("兑换码已经兑换过了");
    }
    try {
        //3.查询兑换码
        ExchangeCode exchangeCode = codeService.getById(serialNum);
        if (exchangeCode == null){
            throw new BizIllegalException("兑换码不存在!");
        }
        //4.是否过期
        LocalDateTime now = LocalDateTime._now_();
        if (now.isAfter(exchangeCode.getExpiredTime())){
            throw new BizIllegalException("兑换码已经过期");
        }
        //5.校验并生成用户券
        //5.1.查询优惠券
        Coupon coupon = couponMapper.selectById(exchangeCode.getExchangeTargetId());
        //5.2.获取用户
        Long userId = UserContext._getUser_();
        checkAndCreateUserCoupon(coupon, userId);
        //6.更新兑换码状态
        codeService.lambdaUpdate()
                .set(ExchangeCode::getUserId, userId)
                .set(ExchangeCode::getStatus, ExchangeCodeStatus._USED_)
                .eq(ExchangeCode::getId, exchangeCode.getId())
                .update();
    } catch (Exception e) {
        //重置兑换的标记 0
        codeService.updateExchangeMark(serialNum, false);
        throw e;
    }
}

这里返回涉及拆箱,需要对空指针进行处理。这里直接传入 serialNum,直接放弃第一个位置,提升性能。

因为.boundValueOps 绑定 KEY 后,没有 setBit 方法,所以还得用 redisTemplate

这个部分跟前面的重复了,直接进行封装,提高代码的复用性。

最后呢,加上事务注解,和抛出异常

测试一下:

并发安全问题超卖问题:

将资料当中的这个,直接拖到 jmeter 左侧栏

然后改下配置

启动前,先去看下数据库当中的优惠券过期时间等信息是否合理。然后启动,如果发现不对,看下后台

启动之后,你就会看到库存超卖的情况

理想情况下:

超卖情况下:

乐观锁实现:

修改相关的代码:

重启服务,恢复数据库数据,重新测试,这个时候线程设为 400 就行

咋就只有了 53 个呢,原先的判断太苛刻了,我们只需要保证 issue_num < total_num 就行了

换回来

经典一人一单问题,悲观锁保证一人一单,乐观锁保证不超卖,配合数据库索引作为兜底

并发安全问题琐失效和锁边界问题:

如果一个人从来没有领过,数据库当中没有他的数据,然后他开启无数个线程,同时攻击,那它就领到了无数张券。

但这里测下来,我并没有出现这个问题,不知道为啥。

那加悲观锁

① 直接在方法上加上 synchronized

直接变成串行执行,那整个业务的性能会受到特别大的影响。

② 将 synchronized 锁的粒度缩小,锁 userId,同步代码块。不同的用户没有影响。同一个用户开启多个线程,才会有影响。

这里 userId 涉黄,方法参数有可能变的

转成字符串常量,这样就好了

测试了一下

怎么还是有问题

原来 toString new 了一个新对象

虽然 userId 相同,但是对象不同,锁就不同

这样就是都获取常量池当中的值了

测试一下,还是 8

理想情况:

事务导致:

线程 1 执行新增后释放锁,实际已经新增了,但是还没有提交事务,事务的隔离级别为读已提交,所以线程 2 看不到,它也新增了。

解决方案:

调换一下提交事务和释放锁的顺序

测试了一下,问题解决了

并发安全问题事务失效问题:

但是这里事务并没有生效,也就是不会回滚,因为是内部调用

一级方法没有添加事务,也就意味着没有被动态代理,也就是原有的 OrderService 调用的,那它内部的方法自然也是它本身调用的,所以不是代理对象调用的,事务不会生效。

要么抛出 RuntimeException 异常,要么就改 rollbackFor 当中的异常类型

本质就是 reduceStock()和原来的不是同个事务,出了异常要回滚,也不相干了。传播行为默认是 REQUIRE ,这里是 REQUIRES_NEW 搞出个新的,看见没

以上就是主要事务失效的原因。

本题属于第二种。

解决方案:

其实这里也可以不引用,因为父类引用过了

启动类添加注解,默认为 false,改为 true

改下实现类

记得抽取一下

把这里事务注解去掉。

ok,搞定。


如果对你有帮助的话,请点赞,关注,收藏。热爱可抵一切!👍 ❤️ 🔥

相关推荐
love530love2 小时前
如何在 Google Chrome 中强制开启 Gemini AI 侧边栏(完整图文教程)
前端·人工智能·chrome·windows
skilllite作者2 小时前
Zed 1.0 编辑器深度评测与实战指南
开发语言·人工智能·windows·python·编辑器·agi
杜哥无敌2 小时前
FreeSSHd vs FileZilla Server vs SFTPGo:Windows SFTP服务器易用性终极横向测评
运维·服务器·windows
李白的天不白2 小时前
vue 数据格式问题
前端·vue.js·windows
宝桥南山12 小时前
AI - 在命令行中尝试一下ACP(Agent Client Protocol)通信
microsoft·微软·github·aigc·copilot
love530love14 小时前
精简版|Claude-HUD 插件介绍 + 一键安装教程
人工智能·windows·笔记
秋915 小时前
MySQL 8.0.46 全平台安装与配置详解(Windows/Linux/macOS)
linux·windows·mysql
善恶怪客15 小时前
LocalSend基本使用
windows
MengMeng_102316 小时前
win10 蓝牙连接音响没有声音设备选项
windows