第12天 优惠卷的使用

怎么解决重复提交订单?

在订单确认页生成一个预订单ID,并返回给前端,真正下订单的时候会把这个id传给后端,把这个id作为数据库主键就可以防止重复提交订单。 idwork.getid()

inner join 和外连接区别

inner join 只返回两个表链结满足条件的,left  right 外连接 不满足条件的

内连接,也被称为自然连接,只有两个表相匹配的行才能在结果集中出现。返回的结果集选取了两个表中所有相匹配的数据,舍弃了不匹配的数据。由于内连接是从结果表中删除与其他连接表中没有匹配的所有行,所以内连接可能会造成信息的丢失。

外连接不仅包含符合连接条件的行,还包含左表(左连接时)、右表(右连接时)或两个边接表(全外连接)中的所有数据行。SQL外连接共有三种类型:左外连接(关键字为LEFT OUTER JOIN)、右外连接(关键字为RIGHT OUTER JOIN)和全外连接(关键字为FULL OUTER JOIN)。外连接的用法和内连接一样,只是将INNER JOIN关键字替换为相应的外连接关键字即可。

内连接只显示符合连接条件的记录,外连接除了显示符合条件的记录外,还显示表中的记录,例如,如果使用右外连接,还显示右表中的记录。

maptoint 

前端传ids =[1,2,3]s时,后端用@RequestParam接收

优惠券使用 

不过,新的问题来了,用户购物的时候自然要选择优惠券来使用。而现在主流的购物网站都会有优惠券的智能推荐功能,那么:

  • 优惠券的类型不同,折扣计算规则该如何用代码表示?

  • 如何组合优惠券使用才能让用户得到最大优惠?

  • 优惠券叠加的计算算法是怎样的?

  • 如果下单时使用了优惠券,用户退款时又该如何处理?

 

优惠券规则定义

 

所谓的优惠券方案推荐,就是从用户的所有优惠券中筛选出可用的优惠券,并且计算哪种优惠方案用券最少,优惠金额最高。

因此这里包含了对优惠券的下列需求:

  • 判断一个优惠券是否可用,也就是检查订单金额是否达到优惠券使用门槛

  • 按照优惠规则计算优惠金额,能够计算才能比较并找出最优方案

  • 生成优惠券规则描述,目的是在页面直观的展示各种方案,供用户选择

package com.tianji.promotion.strategy.discount;

import com.tianji.promotion.domain.po.Coupon;

/**
 * <p>优惠券折扣功能接口</p>
 */
public interface Discount {
    /**
     * 判断当前价格是否满足优惠券使用限制
     * @param totalAmount 订单总价
     * @param coupon 优惠券信息
     * @return 是否可以使用优惠券
     */
    boolean canUse(int totalAmount, Coupon coupon);

    /**
     * 计算折扣金额
     * @param totalAmount 总金额
     * @param coupon 优惠券信息
     * @return 折扣金额
     */
    int calculateDiscount(int totalAmount, Coupon coupon);

    /**
     * 根据优惠券规则返回规则描述信息
     * @return 规则描述信息
     */
    String getRule(Coupon coupon);
}

 

 // 工厂模式

public class DiscountStrategy {

    private final static EnumMap<DiscountType, Discount> strategies;

    static {
        strategies = new EnumMap<>(DiscountType.class);
        strategies.put(DiscountType.NO_THRESHOLD, new NoThresholdDiscount());
        strategies.put(DiscountType.PER_PRICE_DISCOUNT, new PerPriceDiscount());
        strategies.put(DiscountType.RATE_DISCOUNT, new RateDiscount());
        strategies.put(DiscountType.PRICE_DISCOUNT, new PriceDiscount());
    }

    public static Discount getDiscount(DiscountType type) {
        return strategies.get(type);
    }
}

 根据优惠卷的类型得到对象的实现对象,然后判断传过来金额数目,判断对于这个数目这个优惠卷是否可用,优惠金额是多少,规则描述是怎样的

就比如说订单金额1000,这个1000的金额是否达到这个优惠卷的门槛了

 这个是无门槛优惠卷的实现

@RequiredArgsConstructor
public class NoThresholdDiscount implements Discount{

    private static final String RULE_TEMPLATE = "无门槛抵{}元";

    @Override
    public boolean canUse(int totalAmount, Coupon coupon) {
        return totalAmount > coupon.getDiscountValue();
    }

    @Override
    public int calculateDiscount(int totalAmount, Coupon coupon) {
        return coupon.getDiscountValue();
    }

    @Override
    public String getRule(Coupon coupon) {
        return StringUtils.format(RULE_TEMPLATE, NumberUtils.scaleToStr(coupon.getDiscountValue(), 2));
    }
}

优惠券智能推荐

好了,优惠券规则定义好之后,我们就可以正式开发优惠券的相关功能了。

第一个就是优惠券券方案推荐功能。在订单确认页面,前端会向交易微服务发起预下单请求,以获取id和优惠方案列表,页面请求如图:

 交易服务首先需要查询课程信息,生成订单id,然后还需要调用优惠促销服务。而促销服务则需要根据订单中的课程信息查询当前用户的优惠券,并给出推荐的优惠组合方案,供用户在页面选择:

思路分析

简单来说,这就是一个查询优惠券、计算折扣、筛选最优解的过程。整体流程如下:

1.查询用户的可用优惠卷

2.初步筛选(先不看使用范围,先直接把没有达到优惠金额门槛的筛掉)

3.细晒(查询出每个优惠卷的可有范围,查看在这个范围中是否可用)

4. 全排列,对每个排列组合查看优惠卷是否可用 ,优惠金额是多少

5.使用多线程计算优惠金额

6.选择最优方案(卷相同的话,选金额最高的(因为排列顺序不同,优惠金额也可能不同),优惠金额相同,选用卷数量最少的)

 代码分析

首先弄明白返回什么,前端传递的参数是什么

返回的是多个list,每个list中是这套卷组合的优惠金额

参数是课程id 分类 价格

 

 第一步:查询当前用户的优惠卷(记得判断是否为空)

 第二步:初筛(把不能使用的优惠局去掉)

第三步:细筛(根据优惠卷适用范围)

 循环遍历优惠卷是否有限定范围,有限定范围的话去查找该优惠卷限定范围,看限定范围里是否有前端传来的课程,没有下一个循环,有的话看是否达到优惠卷使用门槛,最后放到map集合中。

map中放的就是 优惠卷 对应该优惠卷对应前端传来的课程中可用的课程

private Map<Coupon,List<OrderCourseDTO>> findAvailableCoupons(List<Coupon> coupons,List<OrderCourseDTO> orderCourses){
        Map<Coupon,List<OrderCourseDTO>> map = new HashMap<>();
        //循环遍历初筛后的优惠卷集合
        for (Coupon coupon : coupons) {
            // 2. 找出每一个优惠卷的可用课程,默认都可用,如果有限定范围则删选出去
            List<OrderCourseDTO>  availableCourses = orderCourses;
            // 2.1 判断优惠卷是否限定了范围,没有限定范围就是默认都可用
            if (coupon.getSpecific()){
                //2.2 查询限定范围 查询coupon_scope表
                List<CouponScope> scopeList = couponScopeService.lambdaQuery().eq(CouponScope::getCouponId, coupon.getId()).list();
                // 2.3得到限定范围的id集合
                List<Long> scopeIds = scopeList.stream().map(CouponScope::getCouponId).collect(Collectors.toList());
                // 2.4 从ordercourses 订单中所有的课程集合 筛选该范围内的课程
                availableCourses = orderCourses.stream().filter(new Predicate<OrderCourseDTO>() {
                    @Override
                    public boolean test(OrderCourseDTO orderCourseDTO) {
                        return scopeIds.contains(orderCourseDTO.getCateId());
                    }
                }).collect(Collectors.toList());
                if (CollUtils.isEmpty(availableCourses)){
                    continue;  // 没有可用课程,直接下一次循环
                }
            }
            // 3.计算该优惠卷是否可用 如果可用 添加到map
            int totalSum = availableCourses.stream().mapToInt(OrderCourseDTO::getPrice).sum();
            // 判断优惠卷是否可用 如果可用 则添加到map
            Discount discount = getDiscount(coupon.getDiscountType());
            if (discount.canUse(totalSum,coupon)){
                map.put(coupon,availableCourses);
            }
        }
        return map;
    }

第四步 全排列

全排列的工具类

/**
 * 基于回溯算法的全排列工具类
 */
public class PermuteUtil {
    /**
     * 将[0~n)的所有数字重组,生成不重复的所有排列方案
     *
     * @param n 数字n
     * @return 排列组合
     */
    public static List<List<Byte>> permute(int n) {
        List<List<Byte>> res = new ArrayList<>();

        List<Byte> input = new ArrayList<>(n);
        for (byte i = 0; i < n; i++) {
            input.add(i);
        }

        backtrack(n, input, res, 0);
        return res;
    }

    /**
     * 将指定集合中的元素重组,生成所有的排列组合方案
     *
     * @param input 输入的集合
     * @param <T>   集合类型
     * @return 重组后的集合方案
     */
    public static <T> List<List<T>> permute(List<T> input) {
        List<List<T>> res = new ArrayList<>();
        backtrack(input.size(), input, res, 0);
        return res;
    }

    private static <T> void backtrack(int n, List<T> input, List<List<T>> res, int first) {
        // 所有数都填完了
        if (first == n) {
            res.add(new ArrayList<>(input));
        }
        for (int i = first; i < n; i++) {
            // 动态维护数组
            Collections.swap(input, first, i);
            // 继续递归填下一个数
            backtrack(n, input, res, first + 1);
            // 撤销操作
            Collections.swap(input, first, i);
        }
    }
}

细筛之后得到map,然后取得map中所有的key,对这些key做全排列,并添加 该组合中对应的单卷

 第五步 对每套排列组合做循环,得到每一套组合的优惠明细

这个dto是用来记录,使用了方案后优惠的明细

 detailmap是为了记录使用了某个优惠卷之后 每个课程的优惠价格,一开始初始化为优惠金额为0

    /**
     * 查看每一种优惠卷排序方案对应的优惠卷优惠明细
     * @param avaMap  能用的优惠卷 对应订单中可用的课程的map
     * @param courses 订单中的课程
     * @param solution  优惠卷使用顺序
     * @return   这一套优惠卷使用顺序的 优惠价格等
     */
    private CouponDiscountDTO calculateSolutionDiscount(Map<Coupon,List<OrderCourseDTO>> avaMap,
                                                        List<OrderCourseDTO> courses,
                                                        List<Coupon> solution){
        //1.创建方案结果dto对象
        CouponDiscountDTO dto = new CouponDiscountDTO();
        // 2. 初始化商品id 和 商品折扣明细的映射,初始折扣明细全为0,设置map key为 商品的id  value初始值都为0
        Map<Long, Integer> detailMap = courses.stream().collect(Collectors.toMap(OrderCourseDTO::getId, c -> 0));
        // 3. 循环方案,计算优惠信息
        for (Coupon coupon : solution) {
            // 得出该优惠卷对应的可使用课程
            List<OrderCourseDTO> availableCourses = avaMap.get(coupon);
            //  计算可用课程的总金额(商品价格-该课程的折扣明细)
            int totalAmount = availableCourses.stream().mapToInt(value -> value.getPrice() - detailMap.get(value.getId())).sum();
            // 判断优惠卷是否可用
            Discount discount = getDiscount(coupon.getDiscountType());
            if (!discount.canUse(totalAmount,coupon)){
                continue; // 不可用 跳出循环 继续处理下一次循环
            }
            // 计算该优惠卷使用后的折扣值
            int discountAmount = discount.calculateDiscount(totalAmount, coupon);
            // 计算商品的折扣明细 更新到 detailMap
            calculateDiscountDetails(detailMap, availableCourses, totalAmount, discountAmount);
            // 累加每一个优惠卷的优惠金额 赋值给方案结果dto对象
            dto.getIds().add(coupon.getId()); // 只要执行这句话一维这这个优惠卷生效了
            dto.getRules().add(discount.getRule(coupon));
            dto.setDiscountAmount(discountAmount + dto.getDiscountAmount()); // 不能覆盖 应该是所有生效的优惠卷 累加的结果
        }
        return dto;
    }

 计算折扣明细,为了防止出现无穷,最后一个课程的折扣金额用   总的折扣金额 - 前面的课程的折扣金额

多线程改造计算优惠明细

 CountDownLatch latch = new CountDownLatch(solutions.size());
        for (List<Coupon> solution : solutions) {
            CompletableFuture.supplyAsync(new Supplier<CouponDiscountDTO>() {
                @Override
                public CouponDiscountDTO get() {
                    CouponDiscountDTO dto = calculateSolutionDiscount(availableCouponMap,orderCourses,solution);
                    return dto;
                }
            },discountSolutionExecutor).thenAccept(new Consumer<CouponDiscountDTO>() {   // 上面return的dto就是下面的参数
                @Override
                public void accept(CouponDiscountDTO dto) {
                    dtos.add(dto);
                    latch.countDown(); // 计数器减一
                }
            });
            try {
                latch.await(2, TimeUnit.SECONDS);
            } catch (InterruptedException e) {
                log.error("多线程计算优惠明细报错!!!");
            }
        }

 最后一步:计算最优解

现在,我们计算出了成吨的优惠方案及其优惠金额,但是该如何从其中筛选出最优方案呢?最优方案的标准又是什么呢?

首先来看最优标准:

  • 用券相同时,优惠金额最高的方案

  • 优惠金额相同时,用券最少的方案

其实寻找最优解的流程跟找数组中最小值类似:

  • 定义一个变量记录最小值

  • 逐个遍历数组,判断当前元素是否比最小值更小

  • 如果是,则覆盖最小值;如果否,则放弃

  • 循环结束,变量中记录的就是最小值

例子:

比如 最开始 卷1 3 2 优惠金额是50,放入两个map中,然后是 卷 1 2 3 优惠金额20,小于之前的优惠金额,跳过,卷3 2 1 优惠金额是70,则会替换左边map的优惠方案,同时在右边map中新增一个70的键值对,卷1 2优惠金额也是70,则会替换掉原来70的值。这样下来,左边的map 同一个组合的只有一个。取交集正好满足最优方案。

       private List<CouponDiscountDTO> findBestSolution(List<CouponDiscountDTO> solutions) {
        // 1. 创建两个map 分别记录用卷相同,金额最高     金额相同,用卷最少
        Map<String ,CouponDiscountDTO> moreDiscountMap = new HashMap<>();
        Map<Integer ,CouponDiscountDTO> lessCouponMap = new HashMap<>();
        // 2 循环方案,向map中记录  用卷相同 金额最高       金额相同,用卷最少
        for (CouponDiscountDTO solution : solutions) {
            // 2.1 对优惠卷id升序,转字符串然后以逗号拼接
            String ids = solution.getIds().stream().sorted(Comparator.comparing(Long::longValue)).map(String::valueOf).collect(Collectors.joining(","));
            // 2.2 从 moreDiscountMap中取 旧的记录判断旧方案是否大于等于 当前优惠方案
            CouponDiscountDTO old = moreDiscountMap.get(ids);
            if (old!=null && old.getDiscountAmount()>= solution.getDiscountAmount()){
                continue;
            }
            // 2.从lessCouponMap中取旧的记录 判断旧的方案用卷数量 小于当前方案用卷数量
            old = lessCouponMap.get(solution.getDiscountAmount());
            int newSize = solution.getIds().size(); //当前方案的用卷数量
            if (old!=null && newSize>1 && old.getIds().size() <= newSize){
                continue;
            }
            moreDiscountMap.put(ids,solution);
            lessCouponMap.put(solution.getDiscountAmount(),solution);
        }
        Collection<CouponDiscountDTO> bestSolution = CollUtils.intersection(moreDiscountMap.values(), lessCouponMap.values());
        // 排序 优惠金额降序
        return bestSolution.stream()
                .sorted(Comparator.comparing(CouponDiscountDTO::getDiscountAmount).reversed()).collect(Collectors.toList());
        }

多线程

  • 6
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值