《天机学堂》day09.优惠券管理相关接口_完整代码【简单易懂注释版】

目录

1.优惠券管理-新增优惠券

1.1CouponController.java

1.2interface ICouponService

1.3CouponServiceImpl.java

2.优惠券管理-分页查询优惠券

2.1CouponController.java

2.2interface ICouponService

2.3CouponServiceImpl.java

3.优惠券-根据id查询优惠券

3.1CouponController.java

3.2interface ICouponService

3.3CouponServiceImpl.java

4.优惠券-编辑优惠券

4.1CouponController.java

4.2interface ICouponService

4.3CouponServiceImpl.java

5.优惠券-删除优惠券

5.1CouponController.java

5.2interface ICouponService

5.3CouponServiceImpl.java

6.优惠券发放-实现发放接口

6.1CouponController.java

6.2interface ICouponService

6.3CouponServiceImpl.java

7.优惠券发放-异步生成兑换码(上)

7.1CouponServiceImpl.java

7.2interface IExchangeCodeService

7.3ExchangeCodeServiceImpl.java

7.4PromotionApplication.java

8.优惠券发放-异步生成兑换码(下)

8.1生成兑换码工具类01(Base32.java)

8.2生成兑换码工具类02(CodeUtil.java)

8.3把id改成自己填IdType.INPUT(ExchangeCode.java)

8.4CouponController.java

8.5interface ICouponService

8.6CouponServiceImpl.java

8.7interface IExchangeCodeService

8.8ExchangeCodeServiceImpl.java

9.优惠券管理-定时开始发放优惠券

9.1PointsBoardPersistentHandler.java

9.2interface ICouponService

9.3CouponServiceImpl.java

10.优惠券管理-定时结束发放优惠券

10.1PointsBoardPersistentHandler.java

10.2interface ICouponService.java

10.3CouponServiceImpl.java

11.优惠券管理-暂停发放优惠券

11.1CouponController.java

11.2interface ICouponService

11.3CouponServiceImpl.java

12.优惠券管理-分页查询兑换码

12.1ExchangeCodeController.java

12.2interface IExchangeCodeService

12.3ExchangeCodeServiceImpl.java

day10.领取优惠券

1.查询发放中的优惠券

1.1CouponController.java

1.2interface ICouponService

1.3CouponServiceImpl.java

2.解决登录拦截放行问题

2.1bootstrap.yml

3.领取优惠券

3.1UserCouponController.java

3.2interface IUserCouponService

3.3UserCouponServiceImpl.java

3.4CouponMapper.java

4.兑换码兑换优惠券

4.1UserCouponController.java

4.2interface IUserCouponService

4.3UserCouponServiceImpl.java

4.4CouponMapper.java

5.并发安全问题-乐观锁解决超卖问题

5.1UserCouponServiceImpl.java

5.2CouponMapper.java

6.并发安全问题-锁失效和锁边界问题

6.1UserCouponServiceImpl.java

7.并发安全问题-事务失效问题

7.1UserCouponServiceImpl.java

7.2interface IUserCouponService

8.查询发放中的优惠券

8.1UserCouponController.java

8.2interface IUserCouponService

8.3UserCouponServiceImpl.java


1.优惠券管理-新增优惠券

1.1CouponController.java

package com.tianji.promotion.controller;


@RestController
@RequestMapping("/coupons")
@Api(tags = "优惠券相关接口")
@RequiredArgsConstructor
public class CouponController {

    private final ICouponService couponService;

    // TODO 新增优惠券
    @PostMapping
    @ApiOperation("新增优惠券")
    public void saveCoupon(@Valid @RequestBody CouponFormDTO dto){
        couponService.saveCoupon(dto);
    }

}

1.2interface ICouponService

package com.tianji.promotion.service;

public interface ICouponService extends IService<Coupon> {

    // 新增优惠券
    void saveCoupon(@Valid CouponFormDTO dto);
}

1.3CouponServiceImpl.java

package com.tianji.promotion.service.impl;

@Service
@RequiredArgsConstructor
public class CouponServiceImpl extends ServiceImpl<CouponMapper, Coupon> implements ICouponService {

    private final ICouponScopeService scopeService;

    // 新增优惠券
    @Transactional
    @Override
    public void saveCoupon(CouponFormDTO dto) {
        // 1.保存优惠券
        // 1.1转po
        Coupon coupon = BeanUtils.copyBean(dto, Coupon.class);
        // 1.2保存
        save(coupon);

        if (!dto.getSpecific()){
            // 没有限定范围
            return;
        }
        // 2.保存限定范围
        List<Long> scopes = dto.getScopes();
        if (CollUtils.isEmpty(scopes)){
            throw new BadRequestException("限定范围不能为空");
        }
        // 2.1转换po
        Long couponId = coupon.getId();
        List<CouponScope> list = scopes.stream().map(bizId -> new CouponScope().setBizId(bizId).setCouponId(couponId)).collect(Collectors.toList());
        // 2.2保存
        scopeService.saveBatch(list);

    }
}

2.优惠券管理-分页查询优惠券

2.1CouponController.java

package com.tianji.promotion.controller;

    // TODO 分页查询优惠券
    @GetMapping("/page")
    @ApiOperation("分页查询优惠券")
    public PageDTO<CouponPageVO> queryCouponPage(CouponQuery query){
        return couponService.queryCouponPage(query);
    }

2.2interface ICouponService

package com.tianji.promotion.service;

// 分页查询优惠券
    PageDTO<CouponPageVO> queryCouponPage(CouponQuery query);

2.3CouponServiceImpl.java

package com.tianji.promotion.service.impl;

// 分页查询优惠券
    @Override
    public PageDTO<CouponPageVO> queryCouponPage(CouponQuery query) {
        Integer type = query.getType();
        Integer status = query.getStatus();
        String name = query.getName();
        // 1.分页查询
        Page<Coupon> page = lambdaQuery()
                .eq(type != null, Coupon::getDiscountType, type)
                .eq(status != null, Coupon::getStatus, status)
                .like(name != null, Coupon::getName, name)
                .page(query.toMpPageDefaultSortByCreateTimeDesc());
        // 2.处理VO
        List<Coupon> records = page.getRecords();
        if (CollUtils.isEmpty(records)){
            return PageDTO.empty(page);
        }
        List<CouponPageVO> list = BeanUtils.copyList(records, CouponPageVO.class);
        // 3.返回
        return PageDTO.of(page,list);
    }

3.优惠券-根据id查询优惠券

3.1CouponController.java

package com.tianji.promotion.controller;

    // TODO 根据id查询优惠券
    @GetMapping("/{id}")
    @ApiOperation("根据id查询优惠券")
    public Coupon getCouponById(@PathVariable Long id){
        return couponService.getCouponById(id);
    }

3.2interface ICouponService

package com.tianji.promotion.service;

 // 根据id查询优惠券
    Coupon getCouponById(Long id);

3.3CouponServiceImpl.java

package com.tianji.promotion.service.impl;

    // 根据id查询优惠券
    @Override
    public Coupon getCouponById(Long id) {
        // 返回
        return lambdaQuery()
                .eq(Coupon::getId, id)
                .one();
    }

4.优惠券-编辑优惠券

4.1CouponController.java

package com.tianji.promotion.controller;

    // TODO 编辑优惠券
    @PutMapping("/{id}")
    @ApiOperation("编辑优惠券")
    public void updateCouponById(@Valid @RequestBody CouponFormDTO dto){
        couponService.updateCouponById(dto);
    }

4.2interface ICouponService

package com.tianji.promotion.service;

    // 编辑优惠券
    void updateCouponById(Long id, CouponFormDTO dto);

4.3CouponServiceImpl.java

package com.tianji.promotion.service.impl;

    // 编辑优惠券
    @Override
    public void updateCouponById(CouponFormDTO dto) {
        // 1.属性拷贝
        Coupon coupon = BeanUtils.copyBean(dto, Coupon.class);
        // 2.更新表数据
        updateById(coupon);
    }

5.优惠券-删除优惠券

5.1CouponController.java

package com.tianji.promotion.controller;

5.2interface ICouponService

package com.tianji.promotion.service;

5.3CouponServiceImpl.java

package com.tianji.promotion.service.impl;

6.优惠券发放-实现发放接口

6.1CouponController.java

package com.tianji.promotion.controller;

    // TODO 发放优惠券
    @PutMapping("/{id}/issue")
    @ApiOperation("发放优惠券")
    public void BeginIssueCoupon(@RequestBody CouponIssueFormDTO dto){
        couponService.BeginIssueCoupon(dto);
    }

6.2interface ICouponService

package com.tianji.promotion.service;

    // 发放优惠券
    void BeginIssueCoupon(CouponIssueFormDTO dto);

6.3CouponServiceImpl.java

package com.tianji.promotion.service.impl;

// 发放优惠券
    @Override
    public void BeginIssueCoupon(CouponIssueFormDTO dto) {
        // 1.查询优惠券
        Coupon coupon = getById(dto.getId());
        if (coupon == null){
            throw new BadRequestException("该优惠券不存在!");
        }
        // 2.判断优惠券状态,是否是暂停或待发放
        if (coupon.getStatus() != CouponStatus.DRAFT && coupon.getStatus() != CouponStatus.PAUSE){
            throw new BizIllegalException("优惠券状态错误!");
        }
        // 3.判断是否是立刻发放
        LocalDateTime issueBeginTime = dto.getIssueBeginTime();
        LocalDateTime now = LocalDateTime.now();
        // 开始时间为null或者发送时间小于等于当前时间,都代表立刻发放
        boolean isBegin = issueBeginTime == null || !issueBeginTime.isAfter(now);
        // 4.更新优惠券
        // 4.1拷贝属性
        Coupon c = BeanUtils.copyBean(dto, Coupon.class);
        // 4.2更新状态
        if (isBegin){
            c.setStatus(CouponStatus.ISSUING);
            c.setIssueBeginTime(now);
        }else {
            c.setStatus(CouponStatus.UN_ISSUE);
        }
        // 4.3写入数据库
        updateById(c);
    }

7.优惠券发放-异步生成兑换码(上)

7.1CouponServiceImpl.java

package com.tianji.promotion.service.impl;

// 发放优惠券
    @Override
    public void BeginIssueCoupon(CouponIssueFormDTO dto) {
        // 1.查询优惠券
        Coupon coupon = getById(dto.getId());
        if (coupon == null){
            throw new BadRequestException("该优惠券不存在!");
        }
        // 2.判断优惠券状态,是否是暂停或待发放
        if (coupon.getStatus() != CouponStatus.DRAFT && coupon.getStatus() != CouponStatus.PAUSE){
            throw new BizIllegalException("优惠券状态错误!");
        }
        // 3.判断是否是立刻发放
        LocalDateTime issueBeginTime = dto.getIssueBeginTime();
        LocalDateTime now = LocalDateTime.now();
        // 开始时间为null或者发送时间小于等于当前时间,都代表立刻发放
        boolean isBegin = issueBeginTime == null || !issueBeginTime.isAfter(now);
        // 4.更新优惠券
        // 4.1拷贝属性
        Coupon c = BeanUtils.copyBean(dto, Coupon.class);
        // 4.2更新状态
        if (isBegin){
            c.setStatus(CouponStatus.ISSUING);
            c.setIssueBeginTime(now);
        }else {
            c.setStatus(CouponStatus.UN_ISSUE);
        }
        // 4.3写入数据库
        updateById(c);
        // 5.判断是否需要生成兑换码,优惠券类型必须是兑换码,优惠券状态必须是待发放
        if (coupon.getObtainWay() == ObtainType.ISSUE && coupon.getStatus() == CouponStatus.DRAFT){
            coupon.setIssueEndTime(c.getIssueEndTime());
            // 异步生成兑换码
            codeService.asyncGenerateCode(coupon);
        }

    }

7.2interface IExchangeCodeService

package com.tianji.promotion.service;

    // 异步生成兑换码
    void asyncGenerateCode(Coupon coupon);

7.3ExchangeCodeServiceImpl.java

package com.tianji.promotion.service.impl;

    // 异步生成兑换码
    @Override
    @Async("generateExchangeCodeExecutor")
    public void asyncGenerateCode(Coupon coupon) {

    }

7.4PromotionApplication.java

开启异步的注解@EnableAsync

package com.tianji.promotion;

@EnableAsync
@SpringBootApplication
@MapperScan("com.tianji.promotion.mapper")
@Slf4j
public class PromotionApplication {
    public static void main(String[] args) throws UnknownHostException {
        SpringApplication app = new SpringApplicationBuilder(PromotionApplication.class).build(args);
        Environment env = app.run(args).getEnvironment();
        String protocol = "http";
        if (env.getProperty("server.ssl.key-store") != null) {
            protocol = "https";
        }
        log.info("--/\n---------------------------------------------------------------------------------------\n\t" +
                        "Application '{}' is running! Access URLs:\n\t" +
                        "Local: \t\t{}://localhost:{}\n\t" +
                        "External: \t{}://{}:{}\n\t" +
                        "Profile(s): \t{}" +
                        "\n---------------------------------------------------------------------------------------",
                env.getProperty("spring.application.name"),
                protocol,
                env.getProperty("server.port"),
                protocol,
                InetAddress.getLocalHost().getHostAddress(),
                env.getProperty("server.port"),
                env.getActiveProfiles());
    }
}

8.优惠券发放-异步生成兑换码(下)

8.1生成兑换码工具类01(Base32.java)

package com.tianji.promotion.utils;

import cn.hutool.core.text.StrBuilder;

/**
 * 将整数转为base32字符的工具,因为是32进制,所以每5个bit位转一次
 */
public class package com.tianji.promotion.utils;

import cn.hutool.core.text.StrBuilder;

/**
 * 将整数转为base32字符的工具,因为是32进制,所以每5个bit位转一次
 */
public class Base32 {
    private final static String baseChars = "6CSB7H8DAKXZF3N95RTMVUQG2YE4JWPL";

    public static String encode(long raw) {
        StrBuilder sb = new StrBuilder();
        while (raw != 0) {
            int i = (int) (raw & 0b11111);
            sb.append(baseChars.charAt(i));
            raw = raw >>> 5;
        }
        return sb.toString();
    }

    public static long decode(String code) {
        long r = 0;
        char[] chars = code.toCharArray();
        for (int i = chars.length - 1; i >= 0; i--) {
            long n = baseChars.indexOf(chars[i]);
            r = r | (n << (5*i));
        }
        return r;
    }

    public static String encode(byte[] raw) {
        StrBuilder sb = new StrBuilder();
        int size = 0;
        int temp = 0;
        for (byte b : raw) {
            if (size == 0) {
                // 取5个bit
                int index = (b >>> 3) & 0b11111;
                sb.append(baseChars.charAt(index));
                // 还剩下3位
                size = 3;
                temp = b & 0b111;
            } else {
                int index = temp << (5 - size) | (b >>> (3 + size) & ((1 << 5 - size) - 1)) ;
                sb.append(baseChars.charAt(index));
                int left = 3 + size;
                size = 0;
                if(left >= 5){
                    index = b >>> (left - 5) & ((1 << 5) - 1);
                    sb.append(baseChars.charAt(index));
                    left = left - 5;
                }
                if(left == 0){
                    continue;
                }
                temp = b & ((1 << left) - 1);
                size = left;
            }
        }
        if(size > 0){
            sb.append(baseChars.charAt(temp));
        }
        return sb.toString();
    }

    public static byte[] decode2Byte(String code) {
        char[] chars = code.toCharArray();
        byte[] bytes = new byte[(code.length() * 5 )/ 8];
        byte tmp = 0;
        byte byteSize = 0;
        int index = 0;
        int i = 0;
        for (char c : chars) {
            byte n = (byte) baseChars.indexOf(c);
            i++;
            if (byteSize == 0) {
                tmp = n;
                byteSize = 5;
            } else {
                int left = Math.min(8 - byteSize, 5);
                if(i == chars.length){
                    bytes[index] =(byte) (tmp << left | (n & ((1 << left) - 1)));
                    break;
                }
                tmp = (byte) (tmp << left | (n >>> (5 - left)));
                byteSize += left;
                if (byteSize >= 8) {
                    bytes[index++] = tmp;
                    byteSize = (byte) (5 - left);
                    if (byteSize == 0) {
                        tmp = 0;
                    } else {
                        tmp = (byte) (n & ((1 << byteSize) - 1));
                    }
                }
            }
        }
        return bytes;
    }
}
 {
    private final static String baseChars = "6CSB7H8DAKXZF3N95RTMVUQG2YE4JWPL";

    public static String encode(long raw) {
        StrBuilder sb = new StrBuilder();
        while (raw != 0) {
            int i = (int) (raw & 0b11111);
            sb.append(baseChars.charAt(i));
            raw = raw >>> 5;
        }
        return sb.toString();
    }

    public static long decode(String code) {
        long r = 0;
        char[] chars = code.toCharArray();
        for (int i = chars.length - 1; i >= 0; i--) {
            long n = baseChars.indexOf(chars[i]);
            r = r | (n << (5*i));
        }
        return r;
    }

    public static String encode(byte[] raw) {
        StrBuilder sb = new StrBuilder();
        int size = 0;
        int temp = 0;
        for (byte b : raw) {
            if (size == 0) {
                // 取5个bit
                int index = (b >>> 3) & 0b11111;
                sb.append(baseChars.charAt(index));
                // 还剩下3位
                size = 3;
                temp = b & 0b111;
            } else {
                int index = temp << (5 - size) | (b >>> (3 + size) & ((1 << 5 - size) - 1)) ;
                sb.append(baseChars.charAt(index));
                int left = 3 + size;
                size = 0;
                if(left >= 5){
                    index = b >>> (left - 5) & ((1 << 5) - 1);
                    sb.append(baseChars.charAt(index));
                    left = left - 5;
                }
                if(left == 0){
                    continue;
                }
                temp = b & ((1 << left) - 1);
                size = left;
            }
        }
        if(size > 0){
            sb.append(baseChars.charAt(temp));
        }
        return sb.toString();
    }

    public static byte[] decode2Byte(String code) {
        char[] chars = code.toCharArray();
        byte[] bytes = new byte[(code.length() * 5 )/ 8];
        byte tmp = 0;
        byte byteSize = 0;
        int index = 0;
        int i = 0;
        for (char c : chars) {
            byte n = (byte) baseChars.indexOf(c);
            i++;
            if (byteSize == 0) {
                tmp = n;
                byteSize = 5;
            } else {
                int left = Math.min(8 - byteSize, 5);
                if(i == chars.length){
                    bytes[index] =(byte) (tmp << left | (n & ((1 << left) - 1)));
                    break;
                }
                tmp = (byte) (tmp << left | (n >>> (5 - left)));
                byteSize += left;
                if (byteSize >= 8) {
                    bytes[index++] = tmp;
                    byteSize = (byte) (5 - left);
                    if (byteSize == 0) {
                        tmp = 0;
                    } else {
                        tmp = (byte) (n & ((1 << byteSize) - 1));
                    }
                }
            }
        }
        return bytes;
    }
}

8.2生成兑换码工具类02(CodeUtil.java)

package com.tianji.promotion.utils;

import com.tianji.common.constants.RegexConstants;
import com.tianji.common.exceptions.BadRequestException;

/**
 * <h1 style='font-weight:500'>1.兑换码算法说明:</h1>
 * <p>兑换码分为明文和密文,明文是50位二进制数,密文是长度为10的Base32编码的字符串 </p>
 * <h1 style='font-weight:500'>2.兑换码的明文结构:</h1>
 * <p style='padding: 0 15px'>14(校验码) + 4 (新鲜值) + 32(序列号) </p>
 *   <ul style='padding: 0 15px'>
 *       <li>序列号:一个单调递增的数字,可以通过Redis来生成</li>
 *       <li>新鲜值:可以是优惠券id的最后4位,同一张优惠券的兑换码就会有一个相同标记</li>
 *       <li>载荷:将新鲜值(4位)拼接序列号(32位)得到载荷</li>
 *       <li>校验码:将载荷4位一组,每组乘以加权数,最后累加求和,然后对2^14求余得到</li>
 *   </ul>
 *  <h1 style='font-weight:500'>3.兑换码的加密过程:</h1>
 *     <ol type='a' style='padding: 0 15px'>
 *         <li>首先利用优惠券id计算新鲜值 f</li>
 *         <li>将f和序列号s拼接,得到载荷payload</li>
 *         <li>然后以f为角标,从提前准备好的16组加权码表中选一组</li>
 *         <li>对payload做加权计算,得到校验码 c  </li>
 *         <li>利用c的后4位做角标,从提前准备好的异或密钥表中选择一个密钥:key</li>
 *         <li>将payload与key做异或,作为新payload2</li>
 *         <li>然后拼接兑换码明文:f (4位) + payload2(36位)</li>
 *         <li>利用Base32对密文转码,生成兑换码</li>
 *     </ol>
 * <h1 style='font-weight:500'>4.兑换码的解密过程:</h1>
 * <ol type='a' style='padding: 0 15px'>
 *      <li>首先利用Base32解码兑换码,得到明文数值num</li>
 *      <li>取num的高14位得到c1,取num低36位得payload </li>
 *      <li>利用c1的后4位做角标,从提前准备好的异或密钥表中选择一个密钥:key</li>
 *      <li>将payload与key做异或,作为新payload2</li>
 *      <li>利用加密时的算法,用payload2和s1计算出新校验码c2,把c1和c2比较,一致则通过 </li>
 * </ol>
 */
public class CodeUtil {
    /**
     * 异或密钥表,用于最后的数据混淆
     */
    private final static long[] XOR_TABLE = {
            61261925471L, 61261925523L, 58169127203L, 64169927267L,
            64169927199L, 61261925629L, 58169127227L, 64169927363L,
            59169127063L, 64169927359L, 58169127291L, 61261925739L,
            59169127133L, 55139281911L, 56169127077L, 59169127167L
    };
    /**
     * fresh值的偏移位数
     */
    private final static int FRESH_BIT_OFFSET = 32;
    /**
     * 校验码的偏移位数
     */
    private final static int CHECK_CODE_BIT_OFFSET = 36;
    /**
     * fresh值的掩码,4位
     */
    private final static int FRESH_MASK = 0xF;
    /**
     * 验证码的掩码,14位
     */
    private final static int CHECK_CODE_MASK = 0b11111111111111;
    /**
     * 载荷的掩码,36位
     */
    private final static long PAYLOAD_MASK = 0xFFFFFFFFFL;
    /**
     * 序列号掩码,32位
     */
    private final static long SERIAL_NUM_MASK = 0xFFFFFFFFL;
    /**
     * 序列号加权运算的秘钥表
     */
    private final static int[][] PRIME_TABLE = {
            {23, 59, 241, 61, 607, 67, 977, 1217, 1289, 1601},
            {79, 83, 107, 439, 313, 619, 911, 1049, 1237},
            {173, 211, 499, 673, 823, 941, 1039, 1213, 1429, 1259},
            {31, 293, 311, 349, 431, 577, 757, 883, 1009, 1657},
            {353, 23, 367, 499, 599, 661, 719, 929, 1301, 1511},
            {103, 179, 353, 467, 577, 691, 811, 947, 1153, 1453},
            {213, 439, 257, 313, 571, 619, 743, 829, 983, 1103},
            {31, 151, 241, 349, 607, 677, 769, 823, 967, 1049},
            {61, 83, 109, 137, 151, 521, 701, 827, 1123},
            {23, 61, 199, 223, 479, 647, 739, 811, 947, 1019},
            {31, 109, 311, 467, 613, 743, 821, 881, 1031, 1171},
            {41, 173, 367, 401, 569, 683, 761, 883, 1009, 1181},
            {127, 283, 467, 577, 661, 773, 881, 967, 1097, 1289},
            {59, 137, 257, 347, 439, 547, 641, 839, 977, 1009},
            {61, 199, 313, 421, 613, 739, 827, 941, 1087, 1307},
            {19, 127, 241, 353, 499, 607, 811, 919, 1031, 1301}
    };

    /**
     * 生成兑换码
     *
     * @param serialNum 递增序列号
     * @return 兑换码
     */
    public static String generateCode(long serialNum, long fresh) {
        // 1.计算新鲜值
        fresh = fresh & FRESH_MASK;
        // 2.拼接payload,fresh(4位) + serialNum(32位)
        long payload = fresh << FRESH_BIT_OFFSET | serialNum;
        // 3.计算验证码
        long checkCode = calcCheckCode(payload, (int) fresh);
        System.out.println("checkCode = " + checkCode);
        // 4.payload做大质数异或运算,混淆数据
        payload ^= XOR_TABLE[(int) (checkCode & FRESH_MASK)];
        // 5.拼接兑换码明文: 校验码(14位) + payload(36位)
        long code = checkCode << CHECK_CODE_BIT_OFFSET | payload;
        // 6.转码
        return Base32.encode(code);
    }

    private static long calcCheckCode(long payload, int fresh) {
        // 1.获取码表
        int[] table = PRIME_TABLE[fresh];
        // 2.生成校验码,payload每4位乘加权数,求和,取最后13位结果
        long sum = 0;
        int index = 0;
        while (payload > 0) {
            sum += (payload & 0xf) * table[index++];
            payload >>>= 4;
        }
        return sum & CHECK_CODE_MASK;
    }

    public static long parseCode(String code) {
        if (code == null || !code.matches(RegexConstants.COUPON_CODE_PATTERN)) {
            // 兑换码格式错误
            throw new BadRequestException("无效兑换码");
        }
        // 1.Base32解码
        long num = Base32.decode(code);
        // 2.获取低36位,payload
        long payload = num & PAYLOAD_MASK;
        // 3.获取高14位,校验码
        int checkCode = (int) (num >>> CHECK_CODE_BIT_OFFSET);
        // 4.载荷异或大质数,解析出原来的payload
        payload ^= XOR_TABLE[(checkCode & FRESH_MASK)];
        // 5.获取高4位,fresh
        int fresh = (int) (payload >>> FRESH_BIT_OFFSET & FRESH_MASK);
        // 6.验证格式:
        if (calcCheckCode(payload, fresh) != checkCode) {
            throw new BadRequestException("无效兑换码");
        }
        return payload & SERIAL_NUM_MASK;
    }
}

8.3把id改成自己填IdType.INPUT(ExchangeCode.java)

一定一定要改

@TableId(value = "id", type = IdType.INPUT)

private Integer id;

package com.tianji.promotion.domain.po;

@Data
@EqualsAndHashCode(callSuper = false)
@Accessors(chain = true)
@TableName("exchange_code")
public class ExchangeCode implements Serializable {

    private static final long serialVersionUID = 1L;

    /**
     * 兑换码id
     */
    @TableId(value = "id", type = IdType.INPUT)
    private Integer id;

    /**
     * 兑换码
     */
    private String code;

    /**
     * 兑换码状态, 1:待兑换,2:已兑换,3:兑换活动已结束
     */
    private ExchangeCodeStatus status;

    /**
     * 兑换人
     */
    private Long userId;

    /**
     * 兑换类型,1:优惠券,以后再添加其它类型
     */
    private Integer type;

    /**
     * 兑换码目标id,例如兑换优惠券,该id则是优惠券的配置id
     */
    private Long exchangeTargetId;

    /**
     * 创建时间
     */
    private LocalDateTime createTime;

    /**
     * 兑换码过期时间
     */
    private LocalDateTime expiredTime;

    /**
     * 更新时间
     */
    private LocalDateTime updateTime;


}

8.4CouponController.java

package com.tianji.promotion.controller;

    // TODO 发放优惠券
    @PutMapping("/{id}/issue")
    @ApiOperation("发放优惠券")
    public void BeginIssueCoupon(@RequestBody CouponIssueFormDTO dto){
        couponService.BeginIssueCoupon(dto);
    }

8.5interface ICouponService

package com.tianji.promotion.service;

    // 发放优惠券
    void BeginIssueCoupon(CouponIssueFormDTO dto);

8.6CouponServiceImpl.java

package com.tianji.promotion.service.impl;

    // 发放优惠券
    @Override
    public void BeginIssueCoupon(CouponIssueFormDTO dto) {
        // 1.查询优惠券
        Coupon coupon = getById(dto.getId());
        if (coupon == null){
            throw new BadRequestException("该优惠券不存在!");
        }
        // 2.判断优惠券状态,是否是暂停或待发放
        if (coupon.getStatus() != CouponStatus.DRAFT && coupon.getStatus() != CouponStatus.PAUSE){
            throw new BizIllegalException("优惠券状态错误!");
        }
        // 3.判断是否是立刻发放
        LocalDateTime issueBeginTime = dto.getIssueBeginTime();
        LocalDateTime now = LocalDateTime.now();
        // 开始时间为null或者发送时间小于等于当前时间,都代表立刻发放
        boolean isBegin = issueBeginTime == null || !issueBeginTime.isAfter(now);
        // 4.更新优惠券
        // 4.1拷贝属性
        Coupon c = BeanUtils.copyBean(dto, Coupon.class);
        // 4.2更新状态
        if (isBegin){
            c.setStatus(CouponStatus.ISSUING);
            c.setIssueBeginTime(now);
        }else {
            c.setStatus(CouponStatus.UN_ISSUE);
        }
        // 4.3写入数据库
        updateById(c);
        // 5.判断是否需要生成兑换码,优惠券类型必须是兑换码,优惠券状态必须是待发放
        if (coupon.getObtainWay() == ObtainType.ISSUE && coupon.getStatus() == CouponStatus.DRAFT){
            coupon.setIssueEndTime(c.getIssueEndTime());
            // 异步生成兑换码
            codeService.asyncGenerateCode(coupon);
        }
    }

8.7interface IExchangeCodeService

package com.tianji.promotion.service;

    // 异步生成兑换码
    void asyncGenerateCode(Coupon coupon);

8.8ExchangeCodeServiceImpl.java

package com.tianji.promotion.service.impl;

@Service
public class ExchangeCodeServiceImpl extends ServiceImpl<ExchangeCodeMapper, ExchangeCode> implements IExchangeCodeService {

    private BoundValueOperations<String, String> serialOps;

    public ExchangeCodeServiceImpl(StringRedisTemplate redisTemplate) {
        this.serialOps = redisTemplate.boundValueOps(COUPON_CODE_SERIAL_KEY);
    }

    // 异步生成兑换码
    @Override
    @Async("generateExchangeCodeExecutor")
    public void asyncGenerateCode(Coupon coupon) {
        // 发放数量
        Integer totalNum = coupon.getTotalNum();
        // 1.获取Redis自增序列号
        Long result = serialOps.increment(totalNum);
        if (result == null) {
            return;
        }
        int maxSerialNum = result.intValue();
        List<ExchangeCode> list = new ArrayList<>(totalNum);
        for (int serialNum = maxSerialNum - totalNum + 1; serialNum <= maxSerialNum; serialNum++) {
            // 2.生成兑换码
            String code = CodeUtil.generateCode(serialNum, coupon.getId());
            ExchangeCode e = new ExchangeCode();
            e.setCode(code);
            e.setId(serialNum);
            e.setExchangeTargetId(coupon.getId());
            e.setExpiredTime(coupon.getIssueEndTime());
            list.add(e);
        }
        // 3.保存数据库
        saveBatch(list);
    }
}

9.优惠券管理-定时开始发放优惠券

9.1PointsBoardPersistentHandler.java

package com.tianji.promotion.handler;

@Component
@Slf4j
@RequiredArgsConstructor
public class PointsBoardPersistentHandler {

    private final ICouponService couponService;


    // 定时开始发放优惠券
    @XxlJob("BeginIssueCoupon")
    public void BeginIssueCoupon(){
        // 定时更新优惠券状态
        couponService.BeginIssueCoupon();
    }

}

9.2interface ICouponService

package com.tianji.promotion.service;

    // 定时开始发放优惠券
    void BeginIssueCoupon();

9.3CouponServiceImpl.java

package com.tianji.promotion.service.impl;

// 定时开始发放优惠券(支持分片处理)
    @Override
    public void BeginIssueCoupon() {
        LocalDateTime now = LocalDateTime.now();
        int index = XxlJobHelper.getShardIndex();
        int total = XxlJobHelper.getShardTotal();
        int pageNo = index + 1;
        int pageSize = 1000;

        Page<Coupon> page;
        List<Coupon> records;

        while (true) {
            // 分页查询 状态为【未开始】 且 发放开始时间 <= 当前时间 的优惠券
            page = lambdaQuery()
                    .eq(Coupon::getStatus, CouponStatus.UN_ISSUE)
                    .le(Coupon::getIssueBeginTime, now)
                    .page(new Page<>(pageNo, pageSize));

            pageNo += total; // 分片后翻页
            records = page.getRecords();
            if (CollUtils.isEmpty(records)) {
                log.debug("暂无需要发放的优惠券");
                break;
            }

            // 提取所有优惠券 ID
            List<Long> ids = records.stream()
                    .map(Coupon::getId)
                    .collect(Collectors.toList());

            // 批量更新
            lambdaUpdate()
                    .in(Coupon::getId, ids)
                    .eq(Coupon::getStatus, CouponStatus.UN_ISSUE)
                    .set(Coupon::getStatus, CouponStatus.ISSUING)
                    .update();

            log.info("发现 {} 个优惠券需要从【未开始】更新为【发放中】", ids.size());
        }
    }

10.优惠券管理-定时结束发放优惠券

10.1PointsBoardPersistentHandler.java

package com.tianji.promotion.handler;

// 定时结束发放优惠券
    @XxlJob("EndIssueCoupon")
    public void EndIssueCoupon(){
        // 定时结束优惠券状态
        couponService.EndIssueCoupon();
    }

10.2interface ICouponService.java

package com.tianji.promotion.service;

    // 定时结束优惠券状态
    void EndIssueCoupon();

10.3CouponServiceImpl.java

package com.tianji.promotion.service.impl;

// 定时结束优惠券状态(支持分片处理)
    @Override
    public void EndIssueCoupon() {
        LocalDateTime now = LocalDateTime.now();

        // 查询:状态为“发放中” 且 发放结束时间 <= 当前时间
        List<Coupon> list = lambdaQuery()
                .eq(Coupon::getStatus, CouponStatus.ISSUING)  // 正在发放中
                .le(Coupon::getIssueEndTime, now)              // 已过结束时间
                .list();

        if (CollUtils.isEmpty(list)) {
            log.debug("暂无需要结束的优惠券");
            return;
        }

        // 提取所有需要更新的优惠券ID
        List<Long> ids = list.stream()
                .map(Coupon::getId)
                .collect(Collectors.toList());

        // 批量更新为【已结束】
        lambdaUpdate()
                .in(Coupon::getId, ids)
                .eq(Coupon::getStatus, CouponStatus.ISSUING)  // 乐观锁:防止并发重复更新
                .set(Coupon::getStatus, CouponStatus.FINISHED)
                .update();

        log.info("发现 {} 个优惠券需要从【发放中】更新为【已结束】", ids.size());
    }

11.优惠券管理-暂停发放优惠券

11.1CouponController.java

package com.tianji.promotion.controller;

    // TODO 暂停发放优惠券
    @PutMapping("/{id}/pause")
    @ApiOperation("暂停发放优惠券")
    public void stopCouponStatus(@PathVariable Long id){
        couponService.stopCouponStatus(id);
    }

11.2interface ICouponService

package com.tianji.promotion.service;

   // 暂停发放优惠券
    void stopCouponStatus(Long id);

11.3CouponServiceImpl.java

package com.tianji.promotion.service.impl;

// 暂停发放优惠券
    @Override
    public void stopCouponStatus(Long id) {
        // 设置状态为暂停
        lambdaUpdate()
                .set(Coupon::getStatus,CouponStatus.PAUSE)
                .eq(Coupon::getId,id)
                .update();
    }

12.优惠券管理-分页查询兑换码

12.1ExchangeCodeController.java

package com.tianji.promotion.controller;

@RestController
@RequestMapping("/codes")
@Api(tags = "兑换码相关接口")
@RequiredArgsConstructor
public class ExchangeCodeController {

    private final IExchangeCodeService codeService;

    // TODO 分页查询兑换码
    @GetMapping("/page")
    @ApiOperation("分页查询兑换码")
    public PageDTO<ExchangeCodePageVO> queryExchangeCodePage(ExchangeCodeQuery query){
        return codeService.queryExchangeCodePage(query);
    }

}

12.2interface IExchangeCodeService

package com.tianji.promotion.service;

    // 分页查询兑换码
    PageDTO<ExchangeCodePageVO> queryExchangeCodePage(ExchangeCodeQuery query);

12.3ExchangeCodeServiceImpl.java

package com.tianji.promotion.service.impl;

// 分页查询兑换码
    @Override
    public PageDTO<ExchangeCodePageVO> queryExchangeCodePage(ExchangeCodeQuery query) {
        // 分页查询
        Page<ExchangeCode> page = lambdaQuery()
                .eq(query.getId() != null,ExchangeCode::getId, query.getId())
                .eq(query.getStatus() != null,ExchangeCode::getStatus, query.getStatus())
                .page(query.toMpPageDefaultSortByCreateTimeDesc());

        List<ExchangeCode> records = page.getRecords();
        if (CollUtils.isEmpty(records)){
            return PageDTO.empty(page);
        }
        List<ExchangeCodePageVO> voList = new ArrayList<>(records.size());
        for (ExchangeCode e : records) {
            ExchangeCodePageVO vo = new ExchangeCodePageVO();
            vo.setId(e.getId());
            vo.setCode(e.getCode());
            voList.add(vo);
        }

        return PageDTO.of(page,voList);
    }

day10.领取优惠券

1.查询发放中的优惠券

1.1CouponController.java

package com.tianji.promotion.controller;

    // TODO 查询发放中的优惠券列表
    @GetMapping("/list")
    @ApiOperation("查询发放中的优惠券列表")
    public List<CouponVO> queryIssuingCoupon(){
        return couponService.queryIssuingCoupon();
    }

1.2interface ICouponService

package com.tianji.promotion.service;

    // 查询发放中的优惠券列表
    List<CouponVO> queryIssuingCoupon();

1.3CouponServiceImpl.java

package com.tianji.promotion.service.impl;

// 查询发放中的优惠券列表
    @Override
    public List<CouponVO> queryIssuingCoupon() {
        // 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(us -> us.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;
    }

2.解决登录拦截放行问题

2.1bootstrap.yml

server:
  port: 8092  #端口
  tomcat:
    uri-encoding: UTF-8   #服务编码
spring:
  profiles:
    active: dev
  application:
    name: promotion-service
  cloud:
    nacos:
      config:
        file-extension: yaml
        shared-configs: # 共享配置
          - data-id: shared-spring.yaml # 共享spring配置
            refresh: false
          - data-id: shared-redis.yaml # 共享redis配置
            refresh: false
          - data-id: shared-mybatis.yaml # 共享mybatis配置
            refresh: false
          - data-id: shared-logs.yaml # 共享日志配置
            refresh: false
          - data-id: shared-feign.yaml # 共享feign配置
            refresh: false
          - data-id: shared-xxljob.yaml # 共享xxljob配置
            refresh: false
tj:
  swagger:
    enable: true
    enableResponseWrap: true
    package-path: com.tianji.promotion.controller
    title: 天机学堂 - 促销中心接口文档
    description: 该服务包含优惠促销的相关功能
    contact-name: 传智教育·研究院
    contact-url: http://www.itcast.cn/
    contact-email: zhanghuyi@itcast.cn
    version: v1.0
  jdbc:
    database: tj_promotion
  auth:
    resource:
      enable: true  #开启登录拦截的功能
      exclude-login-paths: #添加排除拦截路径
        - /coupons/list

3.领取优惠券

3.1UserCouponController.java

package com.tianji.promotion.controller;

@RestController
@RequestMapping("/user-coupons")
@Api(tags = "用户优惠券相关接口")
@RequiredArgsConstructor
public class UserCouponController {

    private final IUserCouponService userCouponService;

    // TODO 领取优惠券
    @ApiOperation("领取优惠券")
    @PostMapping("/{couponId}/receive")
    public void receiveCoupon(@PathVariable("couponId") Long couponId){
        userCouponService.receiveCoupon(couponId);
    }

}

3.2interface IUserCouponService

package com.tianji.promotion.service;

    // 领取优惠券
    void receiveCoupon(Long couponId);

3.3UserCouponServiceImpl.java

package com.tianji.promotion.service.impl;

@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("优惠券库存不足!");
        }
        // 4.校验每人领取数量
        Long userId = UserContext.getUser();
        // 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.新增一个用户劵
        UserCoupon uc = new UserCoupon();
        uc.setUserId(userId);
        uc.setCouponId(couponId);
        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);
        // 新增保存到数据库
        save(uc);
    }
}

3.4CouponMapper.java

package com.tianji.promotion.mapper;

public interface CouponMapper extends BaseMapper<Coupon> {

    // 更新优惠券的已经发放数量+1
    @Update("update coupon set issue_num = issue_num + 1 where id = #{couponId}")
    void incrIssueNum(Long couponId);
}

4.兑换码兑换优惠券

4.1UserCouponController.java

package com.tianji.promotion.controller;

    // TODO 兑换码兑换优惠券
    @ApiOperation("兑换码兑换优惠券")
    @PostMapping("/{code}/exchange")
    public void exchangeCoupon(@PathVariable("code") String code){
        userCouponService.exchangeCoupon(code);
    }

4.2interface IUserCouponService

package com.tianji.promotion.service;

    // 兑换码兑换优惠券
    void exchangeCoupon(String code);

4.3UserCouponServiceImpl.java

package com.tianji.promotion.service.impl;

@Service
@RequiredArgsConstructor
public class UserCouponServiceImpl extends ServiceImpl<UserCouponMapper, UserCoupon> implements IUserCouponService {

    private final CouponMapper couponMapper;
    private final IExchangeCodeService codeService;

    // 兑换码兑换优惠券
    @Override
    @Transactional
    public void exchangeCoupon(String code) {
        // 1.校验并解析兑换码
        long serialNum = CodeUtil.parseCode(code);
        // 2.校验是否已经兑换(Redis) SETBIT KEY 4 1
        boolean exchanged = codeService.updateExchangeMark(serialNum,true);
        if (exchanged){
            throw new BizIllegalException("兑换码已经被兑换过了");
        }
        try {
            // 3.查询兑换码(MySql)
            ExchangeCode exchangeCode = codeService.getById(serialNum);
            if (exchangeCode == null) {
                throw new BizIllegalException("兑换码不存在");
            }
            // 4.是否过期(MySql)
            LocalDateTime now = LocalDateTime.now();
            if (now.isAfter(exchangeCode.getExpiredTime())){
                throw new BizIllegalException("兑换码已过期");
            }
            // 5.校验每人领取数量
            Coupon coupon = couponMapper.selectById(exchangeCode.getExchangeTargetId());
            Long userId = UserContext.getUser();
            // 4.1统计当前用户对当前优惠券的已经领取数量
            Integer count = lambdaQuery()
                    .eq(UserCoupon::getUserId, userId)
                    .eq(UserCoupon::getCouponId, coupon.getId())
                    .count();
            // 4.2判断
            if (count != null && count >= coupon.getUserLimit()){
                throw new BadRequestException("优惠券领取次数太多,需使用完再次领取!");
            }
            // 5.更新优惠券的已经发放数量+1
            couponMapper.incrIssueNum(coupon.getId());
            // 6.新增一个用户劵
            UserCoupon uc = new UserCoupon();
            uc.setUserId(userId);
            uc.setCouponId(coupon.getId());
            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);
            // 新增保存到数据库
            save(uc);
            // 8.更新兑换码状态
            codeService.lambdaUpdate()
                    .set(ExchangeCode::getUserId,userId)
                    .set(ExchangeCode::getStatus, ExchangeCodeStatus.USED)
                    .eq(ExchangeCode::getId,exchangeCode.getId())
                    .update();
        } catch (Exception e) {
            // 重置兑换的标记 SETBIT KEY 4 0
            codeService.updateExchangeMark(serialNum,false);
            throw e;
        }

    }
}

4.4CouponMapper.java

package com.tianji.promotion.mapper;

public interface CouponMapper extends BaseMapper<Coupon> {

    // 更新优惠券的已经发放数量+1
    @Update("update coupon set issue_num = issue_num + 1 where id = #{couponId}")
    void incrIssueNum(Long couponId);
}

5.并发安全问题-乐观锁解决超卖问题

5.1UserCouponServiceImpl.java

package com.tianji.promotion.service.impl;

@Service
@RequiredArgsConstructor
public class UserCouponServiceImpl extends ServiceImpl<UserCouponMapper, UserCoupon> implements IUserCouponService {

    private final CouponMapper couponMapper;
    private final IExchangeCodeService codeService;

    // 领取优惠券
    @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("优惠券库存不足!");
        }
        // 4.校验每人领取数量
        Long userId = UserContext.getUser();
        // 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
        int c = couponMapper.incrIssueNum(couponId);
        if (c == 0) {
            throw new BizIllegalException("优惠券库存不足");
        }
        // 6.新增一个用户劵
        UserCoupon uc = new UserCoupon();
        uc.setUserId(userId);
        uc.setCouponId(couponId);
        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);
        // 新增保存到数据库
        save(uc);
    }

    // 兑换码兑换优惠券
    @Override
    @Transactional
    public void exchangeCoupon(String code) {
        // 1.校验并解析兑换码
        long serialNum = CodeUtil.parseCode(code);
        // 2.校验是否已经兑换(Redis) SETBIT KEY 4 1
        boolean exchanged = codeService.updateExchangeMark(serialNum,true);
        if (exchanged){
            throw new BizIllegalException("兑换码已经被兑换过了");
        }
        try {
            // 3.查询兑换码(MySql)
            ExchangeCode exchangeCode = codeService.getById(serialNum);
            if (exchangeCode == null) {
                throw new BizIllegalException("兑换码不存在");
            }
            // 4.是否过期(MySql)
            LocalDateTime now = LocalDateTime.now();
            if (now.isAfter(exchangeCode.getExpiredTime())){
                throw new BizIllegalException("兑换码已过期");
            }
            // 5.校验每人领取数量
            Coupon coupon = couponMapper.selectById(exchangeCode.getExchangeTargetId());
            Long userId = UserContext.getUser();
            // 4.1统计当前用户对当前优惠券的已经领取数量
            Integer count = lambdaQuery()
                    .eq(UserCoupon::getUserId, userId)
                    .eq(UserCoupon::getCouponId, coupon.getId())
                    .count();
            // 4.2判断
            if (count != null && count >= coupon.getUserLimit()){
                throw new BadRequestException("优惠券领取次数太多,需使用完再次领取!");
            }
            // 5.更新优惠券的已经发放数量+1
            int c = couponMapper.incrIssueNum(coupon.getId());
            if (c == 0) {
                throw new BizIllegalException("优惠券库存不足");
            }
            // 6.新增一个用户劵
            UserCoupon uc = new UserCoupon();
            uc.setUserId(userId);
            uc.setCouponId(coupon.getId());
            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);
            // 新增保存到数据库
            save(uc);
            // 8.更新兑换码状态
            codeService.lambdaUpdate()
                    .set(ExchangeCode::getUserId,userId)
                    .set(ExchangeCode::getStatus, ExchangeCodeStatus.USED)
                    .eq(ExchangeCode::getId,exchangeCode.getId())
                    .update();
        } catch (Exception e) {
            // 重置兑换的标记 SETBIT KEY 4 0
            codeService.updateExchangeMark(serialNum,false);
            throw e;
        }

    }
}

5.2CouponMapper.java

package com.tianji.promotion.mapper;

public interface CouponMapper extends BaseMapper<Coupon> {

    // 更新优惠券的已经发放数量+1
    @Update("update coupon set issue_num = issue_num + 1 where id = #{couponId} and issue_num < total_num")
    int incrIssueNum(@Param("couponId") Long couponId);
}

6.并发安全问题-锁失效和锁边界问题

校验并生成用户券抽取成checkAndCreateUser方法

6.1UserCouponServiceImpl.java

package com.tianji.promotion.service.impl;

@Service
@RequiredArgsConstructor
public class UserCouponServiceImpl extends ServiceImpl<UserCouponMapper, UserCoupon> implements IUserCouponService {

    private final CouponMapper couponMapper;
    private final IExchangeCodeService codeService;

    // 领取优惠券
    @Override
    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.校验并生成用户券
        // 加入悲观锁
        synchronized (userId.toString().intern()) {
            checkAndCreateUser(userId, coupon);
        }
    }

    // 兑换码兑换优惠券
    @Override
    public void exchangeCoupon(String code) {
        // 1.校验并解析兑换码
        long serialNum = CodeUtil.parseCode(code);
        // 2.校验是否已经兑换(Redis) SETBIT KEY 4 1
        boolean exchanged = codeService.updateExchangeMark(serialNum, true);
        if (exchanged) {
            throw new BizIllegalException("兑换码已经被兑换过了");
        }
        try {
            // 3.查询兑换码(MySql)
            ExchangeCode exchangeCode = codeService.getById(serialNum);
            if (exchangeCode == null) {
                throw new BizIllegalException("兑换码不存在");
            }
            // 4.是否过期(MySql)
            LocalDateTime now = LocalDateTime.now();
            if (now.isAfter(exchangeCode.getExpiredTime())) {
                throw new BizIllegalException("兑换码已过期");
            }
            Coupon coupon = couponMapper.selectById(exchangeCode.getExchangeTargetId());
            Long userId = UserContext.getUser();
            // 5.校验并生成用户券
            // 加入悲观锁
            synchronized (userId.toString().intern()) {
                checkAndCreateUser(userId, coupon);
            }
            // 6.更新兑换码状态
            codeService.lambdaUpdate()
                    .set(ExchangeCode::getUserId, userId)
                    .set(ExchangeCode::getStatus, ExchangeCodeStatus.USED)
                    .eq(ExchangeCode::getId, exchangeCode.getId())
                    .update();
        } catch (Exception e) {
            // 重置兑换的标记 SETBIT KEY 4 0
            codeService.updateExchangeMark(serialNum, false);
            throw e;
        }

    }

    // 校验并生成用户券(方法)
    @Transactional
    public void checkAndCreateUser(Long userId, Coupon coupon) {
        // 1.1统计当前用户对当前优惠券的已经领取数量
        Integer count = lambdaQuery()
                .eq(UserCoupon::getUserId, userId)
                .eq(UserCoupon::getCouponId, coupon.getId())
                .count();
        // 1.2判断
        if (count != null && count >= coupon.getUserLimit()) {
            throw new BadRequestException("优惠券领取次数太多,需使用完再次领取!");
        }
        // 2.更新优惠券的已经发放数量+1
        // 加入乐观锁incrIssueNum
        int c = couponMapper.incrIssueNum(coupon.getId());
        if (c == 0) {
            throw new BizIllegalException("优惠券库存不足");
        }
        // 3.新增一个用户劵
        UserCoupon uc = new UserCoupon();
        uc.setUserId(userId);
        uc.setCouponId(coupon.getId());
        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);
        // 新增保存到数据库
        save(uc);
    }

}

7.并发安全问题-事务失效问题

7.1UserCouponServiceImpl.java

package com.tianji.promotion.service.impl;

@Service
@RequiredArgsConstructor
public class UserCouponServiceImpl extends ServiceImpl<UserCouponMapper, UserCoupon> implements IUserCouponService {

    private final CouponMapper couponMapper;
    private final IExchangeCodeService codeService;

    // 领取优惠券
    @Override
    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.校验并生成用户券
        // 加入悲观锁
        synchronized (userId.toString().intern()) {
            IUserCouponService userCouponService = (IUserCouponService) AopContext.currentProxy();
            userCouponService.checkAndCreateUser(userId, coupon);
        }
    }

    // 兑换码兑换优惠券
    @Override
    public void exchangeCoupon(String code) {
        // 1.校验并解析兑换码
        long serialNum = CodeUtil.parseCode(code);
        // 2.校验是否已经兑换(Redis) SETBIT KEY 4 1
        boolean exchanged = codeService.updateExchangeMark(serialNum, true);
        if (exchanged) {
            throw new BizIllegalException("兑换码已经被兑换过了");
        }
        try {
            // 3.查询兑换码(MySql)
            ExchangeCode exchangeCode = codeService.getById(serialNum);
            if (exchangeCode == null) {
                throw new BizIllegalException("兑换码不存在");
            }
            // 4.是否过期(MySql)
            LocalDateTime now = LocalDateTime.now();
            if (now.isAfter(exchangeCode.getExpiredTime())) {
                throw new BizIllegalException("兑换码已过期");
            }
            Coupon coupon = couponMapper.selectById(exchangeCode.getExchangeTargetId());
            Long userId = UserContext.getUser();
            // 5.校验并生成用户券
            // 加入悲观锁
            synchronized (userId.toString().intern()) {
                IUserCouponService userCouponService = (IUserCouponService) AopContext.currentProxy();
                userCouponService.checkAndCreateUser(userId, coupon);
            }
            // 6.更新兑换码状态
            codeService.lambdaUpdate()
                    .set(ExchangeCode::getUserId, userId)
                    .set(ExchangeCode::getStatus, ExchangeCodeStatus.USED)
                    .eq(ExchangeCode::getId, exchangeCode.getId())
                    .update();
        } catch (Exception e) {
            // 重置兑换的标记 SETBIT KEY 4 0
            codeService.updateExchangeMark(serialNum, false);
            throw e;
        }

    }

    // 校验并生成用户券(方法)
    @Transactional
    @Override
    public void checkAndCreateUser(Long userId, Coupon coupon) {
        // 1.1统计当前用户对当前优惠券的已经领取数量
        Integer count = lambdaQuery()
                .eq(UserCoupon::getUserId, userId)
                .eq(UserCoupon::getCouponId, coupon.getId())
                .count();
        // 1.2判断
        if (count != null && count >= coupon.getUserLimit()) {
            throw new BadRequestException("优惠券领取次数太多,需使用完再次领取!");
        }
        // 2.更新优惠券的已经发放数量+1
        // 加入乐观锁incrIssueNum
        int c = couponMapper.incrIssueNum(coupon.getId());
        if (c == 0) {
            throw new BizIllegalException("优惠券库存不足");
        }
        // 3.新增一个用户劵
        UserCoupon uc = new UserCoupon();
        uc.setUserId(userId);
        uc.setCouponId(coupon.getId());
        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);
        // 新增保存到数据库
        save(uc);
    }

}

7.2interface IUserCouponService

package com.tianji.promotion.service;

public interface IUserCouponService extends IService<UserCoupon> {

    // 校验并生成用户券(方法)
    void checkAndCreateUser(Long userId, Coupon coupon);
}

8.查询发放中的优惠券

8.1UserCouponController.java

package com.tianji.promotion.controller;

    // TODO 查询发放中的优惠券
    @ApiOperation("查询发放中的优惠券")
    @GetMapping("/page")
    public PageDTO<UserCouponPageVO> queryUserCouponPage(UserCouponQuery query){
        return userCouponService.queryUserCouponPage(query);
    }

8.2interface IUserCouponService

package com.tianji.promotion.service;

    // 查询发放中的优惠券
    PageDTO<UserCouponPageVO> queryUserCouponPage(UserCouponQuery query);

8.3UserCouponServiceImpl.java

package com.tianji.promotion.service.impl;

 // 查询发放中的优惠券
    @Override
    public PageDTO<UserCouponPageVO> queryUserCouponPage(UserCouponQuery query) {
        // 1.分页查询
        Page<UserCoupon> page = lambdaQuery()
                .eq(UserCoupon::getStatus, query.getStatus())
                .page(query.toMpPageDefaultSortByCreateTimeDesc());

        List<UserCoupon> records = page.getRecords();
        if (CollUtils.isEmpty(records)){
            return PageDTO.empty(page);
        }
        // 2.收集优惠券id集合
        List<Long> couponIds = records.stream().map(UserCoupon::getCouponId).collect(Collectors.toList());
        // 3.根据couponId查询coupon表信息
        List<Coupon> coupons = couponMapper.selectBatchIds(couponIds);
        // 4.属性拷贝
        List<UserCouponPageVO> list = BeanUtils.copyList(coupons, UserCouponPageVO.class);

        return PageDTO.of(page,list);
    }
### 天机学堂 Day3 学习内容概述 天机学堂 Day3 的学习重点围绕 **学习计划和进度管理** 展开,主要涉及如何处理用户的课程学习行为并同步更新相关数据[^1]。具体内容如下: #### 一、学习记录初始化与更新逻辑 如果用户正在观看视频类课程,则需先判断该课程是否为首次观看。此操作通过查询数据库中的学习记录实现: - 若无对应的学习记录,则判定为首次观看,并创建新的学习记录条目。 - 如果已有学习记录,则进一步更新当前观看到的具体时间点(以秒计)。这一步骤确保了系统的实时性和准确性。 #### 二、学习完成状态校验 除了基本的播放记录维护外,还需验证本次学习是否满足“学完”的条件。具体而言: - 需要确认播放进度是否达到了预设的比例阈值(通常为95%以上)。 - 同时对比之前的学习状态——若之前的标记是非完成状态而此次确实完成了全部内容,则触发一系列额外的操作来反映这一变化。 #### 三、课表动态调整功能 一旦检测到某门课程由未完成变为已完成的状态转换时,系统将自动执行以下动作之一或多个组合措施: - 更新学员个人主页上的最新学习进展展示模块; - 调整整体教学安排视图内的相应部分,使其他关联项能够及时反映出最新的情况变动。 上述流程不仅限于单一资源类型的跟踪管理,在实际应用过程中可能还会涉及到更多维度的数据交互以及更复杂的业务场景支持需求。 ```python def update_learning_status(user_id, course_id, current_second): """模拟更新学习状态""" # 查询是否有历史记录 record = get_record_by_user_and_course(user_id=user_id, course_id=course_id) if not record: create_new_record(user_id=user_id, course_id=course_id, start_time=current_second) else: last_watched_seconds = record['last_watched'] if is_completed(last_watched_seconds, total_duration_of(course_id)): mark_as_finished(user_id=user_id, course_id=course_id) adjust_curriculum_dashboard(user_id=user_id, finished=True) else: save_progress(record_id=record['id'], new_position=current_second) ```
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值