当前位置: 首页 > news >正文

天机学堂day12学习(完结撒花) - 教程

一、优惠券规则定义

1.1.业务流程分析

1.2.优惠券规则定义

二、优惠券智能推荐

2.1.思路分析

2.2.定义接口

  2.2.1.接口基础信息

  2.2.2.实体准备

  在tj-api dto包下新建 promotion 包导入OrderCourseDTO CouponDiscountDTO

OrderCourseDTO

package com.tianji.api.dto.promotion;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
import lombok.experimental.Accessors;
@Data
@Accessors(chain = true)
@ApiModel(description = "订单中的课程信息")
public class OrderCourseDTO {@ApiModelProperty("课id")private Long id;@ApiModelProperty("课程的三级分类id")private Long cateId;@ApiModelProperty("课程价格")private Integer price;
}

CouponDiscountDTO

package com.tianji.api.dto.promotion;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import lombok.experimental.Accessors;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
@Data
@ApiModel(description = "订单的可用优惠券及折扣信息")
@NoArgsConstructor
@AllArgsConstructor
@Accessors(chain = true)
public class CouponDiscountDTO {@ApiModelProperty("用户优惠券id集合")private List ids = new ArrayList<>();@ApiModelProperty("优惠券规则")private List rules = new ArrayList<>();@ApiModelProperty("本订单最大优惠金额")private Integer discountAmount = 0;
}

  2.2.3.查询我的优惠券可用方案代码实现

    2.2.3.1.UserCouponController
package com.tianji.promotion.controller;
import com.tianji.api.dto.promotion.CouponDiscountDTO;
import com.tianji.api.dto.promotion.OrderCourseDTO;
// ... 略
/*** 

* 用户领取优惠券的记录,是真正使用的优惠券信息 控制器*

** @author 虎哥*/ @RestController @RequiredArgsConstructor @RequestMapping("/user-coupons") @Api(tags = "优惠券相关接口") public class UserCouponController {private final IUserCouponService userCouponService;private final IDiscountService discountService;// ... 略@ApiOperation("查询我的优惠券可用方案")@PostMapping("/available")public List findDiscountSolution(@RequestBody List orderCourses){return discountService.findDiscountSolution(orderCourses);} }
    2.2.3.2.新建 IDiscountService
package com.tianji.promotion.service;
import com.tianji.api.dto.promotion.CouponDiscountDTO;
import com.tianji.api.dto.promotion.OrderCourseDTO;
import java.util.List;
public interface IDiscountService {List findDiscountSolution(List orderCourses);
}
    2.2.3.2.新建 DiscountServiceImpl
package com.tianji.promotion.service.impl;
import com.tianji.api.dto.promotion.CouponDiscountDTO;
import com.tianji.api.dto.promotion.OrderCourseDTO;
import java.util.*;
@Slf4j
@Service
@RequiredArgsConstructor
public class DiscountServiceImpl implements IDiscountService {@Overridepublic List findDiscountSolution(List orderCourses) {//TODOreturn null;}
}

2.3.查询用户券并初步筛选

  2.3.1.编写查询SQL语句

UserCouponMapper

public interface UserCouponMapper extends BaseMapper {List queryMyCoupons(@Param("userId") Long userId);
}

UserCouponMapper.xml


  2.3.2.实现查询和初筛 DiscountServiceImpl

package com.tianji.promotion.service.impl;
import com.tianji.api.dto.promotion.CouponDiscountDTO;
import com.tianji.api.dto.promotion.OrderCourseDTO;
import com.tianji.common.utils.CollUtils;
import com.tianji.common.utils.UserContext;
import com.tianji.promotion.domain.po.Coupon;
import com.tianji.promotion.mapper.UserCouponMapper;
import com.tianji.promotion.service.IDiscountService;
import com.tianji.promotion.strategy.discount.Discount;
import com.tianji.promotion.strategy.discount.DiscountStrategy;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import java.util.*;
import java.util.stream.Collectors;
@Slf4j
@Service
@RequiredArgsConstructor
public class DiscountServiceImpl implements IDiscountService {private final UserCouponMapper userCouponMapper;@Overridepublic List findDiscountSolution(List orderCourses) {// 1.查询我的所有可用优惠券List coupons = userCouponMapper.queryMyCoupons(UserContext.getUser());if (CollUtils.isEmpty(coupons)) {return CollUtils.emptyList();}// 2.初筛// 2.1.计算订单总价int totalAmount = orderCourses.stream().mapToInt(OrderCourseDTO::getPrice).sum();// 2.2.筛选可用券List availableCoupons = coupons.stream().filter(c -> DiscountStrategy.getDiscount(c.getDiscountType()).canUse(totalAmount, c)).collect(Collectors.toList());if (CollUtils.isEmpty(availableCoupons)) {return CollUtils.emptyList();}// 3.排列组合出所有方案// 3.1.细筛(找出每一个优惠券的可用的课程,判断课程总价是否达到优惠券的使用需求)// 3.2.排列组合// 4.计算方案的优惠明细// 5.筛选最优解return null;}
}

2.4.细筛

  在 DiscountServiceImpl 中添加细筛方法 findAvailableCoupon

private final ICouponScopeService scopeService;
private Map> findAvailableCoupon(List coupons, List courses) {Map> map = new HashMap<>(coupons.size());for (Coupon coupon : coupons) {// 1.找出优惠券的可用的课程List availableCourses = courses;if (coupon.getSpecific()) {// 1.1.限定了范围,查询券的可用范围List scopes = scopeService.lambdaQuery().eq(CouponScope::getCouponId, coupon.getId()).list();// 1.2.获取范围对应的分类idSet scopeIds = scopes.stream().map(CouponScope::getBizId).collect(Collectors.toSet());// 1.3.筛选课程availableCourses = courses.stream().filter(c -> scopeIds.contains(c.getCateId())).collect(Collectors.toList());}if (CollUtils.isEmpty(availableCourses)) {// 没有任何可用课程,抛弃continue;}// 2.计算课程总价int totalAmount = availableCourses.stream().mapToInt(OrderCourseDTO::getPrice).sum();// 3.判断是否可用Discount discount = DiscountStrategy.getDiscount(coupon.getDiscountType());if (discount.canUse(totalAmount, coupon)) {map.put(coupon, availableCourses);}}return map;
}

2.5.优惠方案全排列组合

promotion utils 包下导入回溯算法的工具类:PermuteUtil

package com.tianji.promotion.utils;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
/*** 基于回溯算法的全排列工具类*/
public class PermuteUtil {/*** 将[0~n)的所有数字重组,生成不重复的所有排列方案** @param n 数字n* @return 排列组合*/public static List> permute(int n) {List> res = new ArrayList<>();List input = new ArrayList<>(n);for (byte i = 0; i < n; i++) {input.add(i);}backtrack(n, input, res, 0);return res;}/*** 将指定集合中的元素重组,生成所有的排列组合方案** @param input 输入的集合* @param    集合类型* @return 重组后的集合方案*/public static  List> permute(List input) {List> res = new ArrayList<>();backtrack(input.size(), input, res, 0);return res;}private static  void backtrack(int n, List input, List> 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);}}
}

DiscountServiceImpl 中添加细筛全排列的逻辑

package com.tianji.promotion.service.impl;
import com.tianji.api.dto.promotion.CouponDiscountDTO;
import com.tianji.api.dto.promotion.OrderCourseDTO;
import com.tianji.common.utils.CollUtils;
import com.tianji.common.utils.UserContext;
import com.tianji.promotion.domain.po.Coupon;
import com.tianji.promotion.domain.po.CouponScope;
import com.tianji.promotion.mapper.UserCouponMapper;
import com.tianji.promotion.service.ICouponScopeService;
import com.tianji.promotion.service.IDiscountService;
import com.tianji.promotion.strategy.discount.Discount;
import com.tianji.promotion.strategy.discount.DiscountStrategy;
import com.tianji.promotion.utils.PermuteUtil;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import java.util.*;
import java.util.stream.Collectors;
@Slf4j
@Service
@RequiredArgsConstructor
public class DiscountServiceImpl implements IDiscountService {private final UserCouponMapper userCouponMapper;private final ICouponScopeService scopeService;@Overridepublic List findDiscountSolution(List orderCourses) {// 1.查询我的所有可用优惠券List coupons = userCouponMapper.queryMyCoupons(UserContext.getUser());if (CollUtils.isEmpty(coupons)) {return CollUtils.emptyList();}// 2.初筛// 2.1.计算订单总价int totalAmount = orderCourses.stream().mapToInt(OrderCourseDTO::getPrice).sum();// 2.2.筛选可用券List availableCoupons = coupons.stream().filter(c -> DiscountStrategy.getDiscount(c.getDiscountType()).canUse(totalAmount, c)).collect(Collectors.toList());if (CollUtils.isEmpty(availableCoupons)) {return CollUtils.emptyList();}// 3.排列组合出所有方案// 3.1.细筛(找出每一个优惠券的可用的课程,判断课程总价是否达到优惠券的使用需求)Map> availableCouponMap = findAvailableCoupon(availableCoupons, orderCourses);if (CollUtils.isEmpty(availableCouponMap)) {return CollUtils.emptyList();}// 3.2.排列组合availableCoupons = new ArrayList<>(availableCouponMap.keySet());List> solutions = PermuteUtil.permute(availableCoupons);// 3.3.添加单券的方案for (Coupon c : availableCoupons) {solutions.add(List.of(c));}// 4.计算方案的优惠明细// 5.筛选最优解return null;}private Map> findAvailableCoupon(List coupons, List courses) {Map> map = new HashMap<>(coupons.size());for (Coupon coupon : coupons) {// 1.找出优惠券的可用的课程List availableCourses = courses;if (coupon.getSpecific()) {// 1.1.限定了范围,查询券的可用范围List scopes = scopeService.lambdaQuery().eq(CouponScope::getCouponId, coupon.getId()).list();// 1.2.获取范围对应的分类idSet scopeIds = scopes.stream().map(CouponScope::getBizId).collect(Collectors.toSet());// 1.3.筛选课程availableCourses = courses.stream().filter(c -> scopeIds.contains(c.getCateId())).collect(Collectors.toList());}if (CollUtils.isEmpty(availableCourses)) {// 没有任何可用课程,抛弃continue;}// 2.计算课程总价int totalAmount = availableCourses.stream().mapToInt(OrderCourseDTO::getPrice).sum();// 3.判断是否可用Discount discount = DiscountStrategy.getDiscount(coupon.getDiscountType());if (discount.canUse(totalAmount, coupon)) {map.put(coupon, availableCourses);}}return map;}
}

2.5.计算优惠明细

  2.5.1.单张优惠券算法

  单张优惠券的优惠金额计算流程如下:

  • 1)判断优惠券限定范围,找出范围内的课程

  • 2)计算课程总价

  • 3)判断券是否可用

  • 4)计算优惠金额

算法来判断:

  • 1)判断限定范围:这张券限定分类 b,对应的商品序号是2、3

  • 2)计算课程总价:商品序号2、3的总价为200

  • 3)判断是否可用:总价刚好达到优惠券满减门槛200,可以使用

  • 4)计算优惠:满200减100,因此最终优惠金额就是100元

  2.5.2.券叠加算法

  券叠加算法比单券算法需要多一步:

  • 1)判断优惠券限定范围,找出范围内的课程

  • 2)计算课程总价

  • 3)判断券是否可用

  • 4)计算优惠金额

  • 5)计算优惠明细

  2.5.3.编码实现算法 DiscountServiceImpl

package com.tianji.promotion.service.impl;
import com.tianji.api.dto.promotion.CouponDiscountDTO;
import com.tianji.api.dto.promotion.OrderCourseDTO;
import com.tianji.common.utils.CollUtils;
import com.tianji.common.utils.UserContext;
import com.tianji.promotion.domain.po.Coupon;
import com.tianji.promotion.domain.po.CouponScope;
import com.tianji.promotion.mapper.UserCouponMapper;
import com.tianji.promotion.service.ICouponScopeService;
import com.tianji.promotion.service.IDiscountService;
import com.tianji.promotion.strategy.discount.Discount;
import com.tianji.promotion.strategy.discount.DiscountStrategy;
import com.tianji.promotion.utils.PermuteUtil;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import java.util.*;
import java.util.stream.Collectors;
@Slf4j
@Service
@RequiredArgsConstructor
public class DiscountServiceImpl implements IDiscountService {private final UserCouponMapper userCouponMapper;private final ICouponScopeService scopeService;@Overridepublic List findDiscountSolution(List orderCourses) {// 1.查询我的所有可用优惠券List coupons = userCouponMapper.queryMyCoupons(UserContext.getUser());if (CollUtils.isEmpty(coupons)) {return CollUtils.emptyList();}// 2.初筛// 2.1.计算订单总价int totalAmount = orderCourses.stream().mapToInt(OrderCourseDTO::getPrice).sum();// 2.2.筛选可用券List availableCoupons = coupons.stream().filter(c -> DiscountStrategy.getDiscount(c.getDiscountType()).canUse(totalAmount, c)).collect(Collectors.toList());if (CollUtils.isEmpty(availableCoupons)) {return CollUtils.emptyList();}// 3.排列组合出所有方案// 3.1.细筛(找出每一个优惠券的可用的课程,判断课程总价是否达到优惠券的使用需求)Map> availableCouponMap = findAvailableCoupon(availableCoupons, orderCourses);if (CollUtils.isEmpty(availableCouponMap)) {return CollUtils.emptyList();}// 3.2.排列组合availableCoupons = new ArrayList<>(availableCouponMap.keySet());List> solutions = PermuteUtil.permute(availableCoupons);// 3.3.添加单券的方案for (Coupon c : availableCoupons) {solutions.add(List.of(c));}// 4.计算方案的优惠明细List list =Collections.synchronizedList(new ArrayList<>(solutions.size()));for (List solution : solutions) {list.add(calculateSolutionDiscount(availableCouponMap, orderCourses, solution));}// 5.筛选最优解return null;}private CouponDiscountDTO calculateSolutionDiscount(Map> couponMap, List courses, List solution) {// 1.初始化DTOCouponDiscountDTO dto = new CouponDiscountDTO();// 2.初始化折扣明细的映射Map detailMap = courses.stream().collect(Collectors.toMap(OrderCourseDTO::getId, oc -> 0));// 3.计算折扣for (Coupon coupon : solution) {// 3.1.获取优惠券限定范围对应的课程List availableCourses = couponMap.get(coupon);// 3.2.计算课程总价(课程原价 - 折扣明细)int totalAmount = availableCourses.stream().mapToInt(oc -> oc.getPrice() - detailMap.get(oc.getId())).sum();// 3.3.判断是否可用Discount discount = DiscountStrategy.getDiscount(coupon.getDiscountType());if (!discount.canUse(totalAmount, coupon)) {// 券不可用,跳过continue;}// 3.4.计算优惠金额int discountAmount = discount.calculateDiscount(totalAmount, coupon);// 3.5.计算优惠明细calculateDiscountDetails(detailMap, availableCourses, totalAmount, discountAmount);// 3.6.更新DTO数据dto.getIds().add(coupon.getCreater());dto.getRules().add(discount.getRule(coupon));dto.setDiscountAmount(discountAmount + dto.getDiscountAmount());}return dto;}private void calculateDiscountDetails(Map detailMap, List courses,int totalAmount, int discountAmount) {int times = 0;int remainDiscount = discountAmount;for (OrderCourseDTO course : courses) {// 更新课程已计算数量times++;int discount = 0;// 判断是否是最后一个课程if (times == courses.size()) {// 是最后一个课程,总折扣金额 - 之前所有商品的折扣金额之和discount = remainDiscount;} else {// 计算折扣明细(课程价格在总价中占的比例,乘以总的折扣)discount = discountAmount * course.getPrice() / totalAmount;remainDiscount -= discount;}// 更新折扣明细detailMap.put(course.getId(), discount + detailMap.get(course.getId()));}}private Map> findAvailableCoupon(List coupons, List courses) {Map> map = new HashMap<>(coupons.size());for (Coupon coupon : coupons) {// 1.找出优惠券的可用的课程List availableCourses = courses;if (coupon.getSpecific()) {// 1.1.限定了范围,查询券的可用范围List scopes = scopeService.lambdaQuery().eq(CouponScope::getCouponId, coupon.getId()).list();// 1.2.获取范围对应的分类idSet scopeIds = scopes.stream().map(CouponScope::getBizId).collect(Collectors.toSet());// 1.3.筛选课程availableCourses = courses.stream().filter(c -> scopeIds.contains(c.getCateId())).collect(Collectors.toList());}if (CollUtils.isEmpty(availableCourses)) {// 没有任何可用课程,抛弃continue;}// 2.计算课程总价int totalAmount = availableCourses.stream().mapToInt(OrderCourseDTO::getPrice).sum();// 3.判断是否可用Discount discount = DiscountStrategy.getDiscount(coupon.getDiscountType());if (discount.canUse(totalAmount, coupon)) {map.put(coupon, availableCourses);}}return map;}
}

2.6.CompleteableFuture并发计算

新建一个自定义线程池 PromotionConfig

package com.tianji.promotion.config;
import lombok.extern.slf4j.Slf4j;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;
import java.util.concurrent.Executor;
import java.util.concurrent.ThreadPoolExecutor;
@Slf4j
@Configuration
public class PromotionConfig {@Beanpublic Executor generateExchangeCodeExecutor(){ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();// 1.核心线程池大小executor.setCorePoolSize(2);// 2.最大线程池大小executor.setMaxPoolSize(5);// 3.队列大小executor.setQueueCapacity(200);// 4.线程名称executor.setThreadNamePrefix("exchange-code-handler-");// 5.拒绝策略executor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy());executor.initialize();return executor;}//新增discountSolutionExecutor@Beanpublic Executor discountSolutionExecutor(){ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();// 1.核心线程池大小executor.setCorePoolSize(12);// 2.最大线程池大小executor.setMaxPoolSize(12);// 3.队列大小executor.setQueueCapacity(99999);// 4.线程名称executor.setThreadNamePrefix("discount-solution-calculator-");// 5.拒绝策略executor.setRejectedExecutionHandler(new ThreadPoolExecutor.AbortPolicy());executor.initialize();return executor;}
}

修改 DiscountServiceImpl 中查询优惠方案的函数主体

package com.tianji.promotion.service.impl;
import com.tianji.api.dto.promotion.CouponDiscountDTO;
import com.tianji.api.dto.promotion.OrderCourseDTO;
import com.tianji.common.utils.CollUtils;
import com.tianji.common.utils.UserContext;
import com.tianji.promotion.domain.po.Coupon;
import com.tianji.promotion.domain.po.CouponScope;
import com.tianji.promotion.mapper.UserCouponMapper;
import com.tianji.promotion.service.ICouponScopeService;
import com.tianji.promotion.service.IDiscountService;
import com.tianji.promotion.strategy.discount.Discount;
import com.tianji.promotion.strategy.discount.DiscountStrategy;
import com.tianji.promotion.utils.PermuteUtil;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import java.util.*;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.Executor;
import java.util.concurrent.TimeUnit;
import java.util.stream.Collectors;
@Slf4j
@Service
@RequiredArgsConstructor
public class DiscountServiceImpl implements IDiscountService {private final UserCouponMapper userCouponMapper;private final ICouponScopeService scopeService;private final Executor discountSolutionExecutor;@Overridepublic List findDiscountSolution(List orderCourses) {// 1.查询我的所有可用优惠券List coupons = userCouponMapper.queryMyCoupons(UserContext.getUser());if (CollUtils.isEmpty(coupons)) {return CollUtils.emptyList();}// 2.初筛// 2.1.计算订单总价int totalAmount = orderCourses.stream().mapToInt(OrderCourseDTO::getPrice).sum();// 2.2.筛选可用券List availableCoupons = coupons.stream().filter(c -> DiscountStrategy.getDiscount(c.getDiscountType()).canUse(totalAmount, c)).collect(Collectors.toList());if (CollUtils.isEmpty(availableCoupons)) {return CollUtils.emptyList();}// 3.排列组合出所有方案// 3.1.细筛(找出每一个优惠券的可用的课程,判断课程总价是否达到优惠券的使用需求)Map> availableCouponMap = findAvailableCoupon(availableCoupons, orderCourses);if (CollUtils.isEmpty(availableCouponMap)) {return CollUtils.emptyList();}// 3.2.排列组合availableCoupons = new ArrayList<>(availableCouponMap.keySet());List> solutions = PermuteUtil.permute(availableCoupons);// 3.3.添加单券的方案for (Coupon c : availableCoupons) {solutions.add(List.of(c));}// 4.计算方案的优惠明细
//        List list =
//                Collections.synchronizedList(new ArrayList<>(solutions.size()));
//        for (List solution : solutions) {
//            list.add(calculateSolutionDiscount(availableCouponMap, orderCourses, solution));
//        }// 4.计算方案的优惠明细  使用线程池List list = Collections.synchronizedList(new ArrayList<>(solutions.size()));// 4.1.定义闭锁CountDownLatch latch = new CountDownLatch(solutions.size());for (List solution : solutions) {// 4.2.异步计算CompletableFuture.supplyAsync(() -> calculateSolutionDiscount(availableCouponMap, orderCourses, solution),discountSolutionExecutor    //注意注入 private final Executor discountSolutionExecutor).thenAccept(dto -> {// 4.3.提交任务结果list.add(dto);latch.countDown();});}// 4.4.等待运算结束try {latch.await(1, TimeUnit.SECONDS);} catch (InterruptedException e) {log.error("优惠方案计算被中断,{}", e.getMessage());}// 5.筛选最优解return null;}private CouponDiscountDTO calculateSolutionDiscount(Map> couponMap, List courses, List solution) {// 1.初始化DTOCouponDiscountDTO dto = new CouponDiscountDTO();// 2.初始化折扣明细的映射Map detailMap = courses.stream().collect(Collectors.toMap(OrderCourseDTO::getId, oc -> 0));// 3.计算折扣for (Coupon coupon : solution) {// 3.1.获取优惠券限定范围对应的课程List availableCourses = couponMap.get(coupon);// 3.2.计算课程总价(课程原价 - 折扣明细)int totalAmount = availableCourses.stream().mapToInt(oc -> oc.getPrice() - detailMap.get(oc.getId())).sum();// 3.3.判断是否可用Discount discount = DiscountStrategy.getDiscount(coupon.getDiscountType());if (!discount.canUse(totalAmount, coupon)) {// 券不可用,跳过continue;}// 3.4.计算优惠金额int discountAmount = discount.calculateDiscount(totalAmount, coupon);// 3.5.计算优惠明细calculateDiscountDetails(detailMap, availableCourses, totalAmount, discountAmount);// 3.6.更新DTO数据dto.getIds().add(coupon.getCreater());dto.getRules().add(discount.getRule(coupon));dto.setDiscountAmount(discountAmount + dto.getDiscountAmount());}return dto;}private void calculateDiscountDetails(Map detailMap, List courses,int totalAmount, int discountAmount) {int times = 0;int remainDiscount = discountAmount;for (OrderCourseDTO course : courses) {// 更新课程已计算数量times++;int discount = 0;// 判断是否是最后一个课程if (times == courses.size()) {// 是最后一个课程,总折扣金额 - 之前所有商品的折扣金额之和discount = remainDiscount;} else {// 计算折扣明细(课程价格在总价中占的比例,乘以总的折扣)discount = discountAmount * course.getPrice() / totalAmount;remainDiscount -= discount;}// 更新折扣明细detailMap.put(course.getId(), discount + detailMap.get(course.getId()));}}private Map> findAvailableCoupon(List coupons, List courses) {Map> map = new HashMap<>(coupons.size());for (Coupon coupon : coupons) {// 1.找出优惠券的可用的课程List availableCourses = courses;if (coupon.getSpecific()) {// 1.1.限定了范围,查询券的可用范围List scopes = scopeService.lambdaQuery().eq(CouponScope::getCouponId, coupon.getId()).list();// 1.2.获取范围对应的分类idSet scopeIds = scopes.stream().map(CouponScope::getBizId).collect(Collectors.toSet());// 1.3.筛选课程availableCourses = courses.stream().filter(c -> scopeIds.contains(c.getCateId())).collect(Collectors.toList());}if (CollUtils.isEmpty(availableCourses)) {// 没有任何可用课程,抛弃continue;}// 2.计算课程总价int totalAmount = availableCourses.stream().mapToInt(OrderCourseDTO::getPrice).sum();// 3.判断是否可用Discount discount = DiscountStrategy.getDiscount(coupon.getDiscountType());if (discount.canUse(totalAmount, coupon)) {map.put(coupon, availableCourses);}}return map;}
}

2.7.筛选最优解

首先来看最优标准:

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

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

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

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

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

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

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

  其中:

  • 第一个Map用来记录用券相同时,优惠金额最高的方案;

  • 第二个Map用来记录优惠金额相同时,用券最少的方案。

  最终,两个Map的values的交集就是我们要找的最优解。

  最终代码实现

package com.tianji.promotion.service.impl;
import com.tianji.api.dto.promotion.CouponDiscountDTO;
import com.tianji.api.dto.promotion.OrderCourseDTO;
import com.tianji.common.utils.CollUtils;
import com.tianji.common.utils.UserContext;
import com.tianji.promotion.domain.po.Coupon;
import com.tianji.promotion.domain.po.CouponScope;
import com.tianji.promotion.mapper.UserCouponMapper;
import com.tianji.promotion.service.ICouponScopeService;
import com.tianji.promotion.service.IDiscountService;
import com.tianji.promotion.strategy.discount.Discount;
import com.tianji.promotion.strategy.discount.DiscountStrategy;
import com.tianji.promotion.utils.PermuteUtil;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import java.util.*;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.Executor;
import java.util.concurrent.TimeUnit;
import java.util.stream.Collectors;
@Slf4j
@Service
@RequiredArgsConstructor
public class DiscountServiceImpl implements IDiscountService {private final UserCouponMapper userCouponMapper;private final ICouponScopeService scopeService;private final Executor discountSolutionExecutor;@Overridepublic List findDiscountSolution(List orderCourses) {// 1.查询我的所有可用优惠券List coupons = userCouponMapper.queryMyCoupons(UserContext.getUser());if (CollUtils.isEmpty(coupons)) {return CollUtils.emptyList();}// 2.初筛// 2.1.计算订单总价int totalAmount = orderCourses.stream().mapToInt(OrderCourseDTO::getPrice).sum();// 2.2.筛选可用券List availableCoupons = coupons.stream().filter(c -> DiscountStrategy.getDiscount(c.getDiscountType()).canUse(totalAmount, c)).collect(Collectors.toList());if (CollUtils.isEmpty(availableCoupons)) {return CollUtils.emptyList();}// 3.排列组合出所有方案// 3.1.细筛(找出每一个优惠券的可用的课程,判断课程总价是否达到优惠券的使用需求)Map> availableCouponMap = findAvailableCoupon(availableCoupons, orderCourses);if (CollUtils.isEmpty(availableCouponMap)) {return CollUtils.emptyList();}// 3.2.排列组合availableCoupons = new ArrayList<>(availableCouponMap.keySet());List> solutions = PermuteUtil.permute(availableCoupons);// 3.3.添加单券的方案for (Coupon c : availableCoupons) {solutions.add(List.of(c));}// 4.计算方案的优惠明细
//        List list =
//                Collections.synchronizedList(new ArrayList<>(solutions.size()));
//        for (List solution : solutions) {
//            list.add(calculateSolutionDiscount(availableCouponMap, orderCourses, solution));
//        }// 4.计算方案的优惠明细  使用线程池List list = Collections.synchronizedList(new ArrayList<>(solutions.size()));// 4.1.定义闭锁CountDownLatch latch = new CountDownLatch(solutions.size());for (List solution : solutions) {// 4.2.异步计算CompletableFuture.supplyAsync(() -> calculateSolutionDiscount(availableCouponMap, orderCourses, solution),discountSolutionExecutor    //注意注入 private final Executor discountSolutionExecutor).thenAccept(dto -> {// 4.3.提交任务结果list.add(dto);latch.countDown();});}// 4.4.等待运算结束try {latch.await(1, TimeUnit.SECONDS);} catch (InterruptedException e) {log.error("优惠方案计算被中断,{}", e.getMessage());}// 5.筛选最优解return findBestSolution(list);}private List findBestSolution(List list) {// 1.准备Map记录最优解Map moreDiscountMap = new HashMap<>();Map lessCouponMap = new HashMap<>();// 2.遍历,筛选最优解for (CouponDiscountDTO solution : list) {// 2.1.计算当前方案的id组合String ids = solution.getIds().stream().sorted(Long::compare).map(String::valueOf).collect(Collectors.joining(","));// 2.2.比较用券相同时,优惠金额是否最大CouponDiscountDTO best = moreDiscountMap.get(ids);if (best != null && best.getDiscountAmount() >= solution.getDiscountAmount()) {// 当前方案优惠金额少,跳过continue;}// 2.3.比较金额相同时,用券数量是否最少best = lessCouponMap.get(solution.getDiscountAmount());int size = solution.getIds().size();if (size > 1 && best != null && best.getIds().size() <= size) {// 当前方案用券更多,放弃continue;}// 2.4.更新最优解moreDiscountMap.put(ids, solution);lessCouponMap.put(solution.getDiscountAmount(), solution);}// 3.求交集Collection bestSolutions = CollUtils.intersection(moreDiscountMap.values(), lessCouponMap.values());// 4.排序,按优惠金额降序return bestSolutions.stream().sorted(Comparator.comparingInt(CouponDiscountDTO::getDiscountAmount).reversed()).collect(Collectors.toList());}private CouponDiscountDTO calculateSolutionDiscount(Map> couponMap, List courses, List solution) {// 1.初始化DTOCouponDiscountDTO dto = new CouponDiscountDTO();// 2.初始化折扣明细的映射Map detailMap = courses.stream().collect(Collectors.toMap(OrderCourseDTO::getId, oc -> 0));// 3.计算折扣for (Coupon coupon : solution) {// 3.1.获取优惠券限定范围对应的课程List availableCourses = couponMap.get(coupon);// 3.2.计算课程总价(课程原价 - 折扣明细)int totalAmount = availableCourses.stream().mapToInt(oc -> oc.getPrice() - detailMap.get(oc.getId())).sum();// 3.3.判断是否可用Discount discount = DiscountStrategy.getDiscount(coupon.getDiscountType());if (!discount.canUse(totalAmount, coupon)) {// 券不可用,跳过continue;}// 3.4.计算优惠金额int discountAmount = discount.calculateDiscount(totalAmount, coupon);// 3.5.计算优惠明细calculateDiscountDetails(detailMap, availableCourses, totalAmount, discountAmount);// 3.6.更新DTO数据dto.getIds().add(coupon.getCreater());dto.getRules().add(discount.getRule(coupon));dto.setDiscountAmount(discountAmount + dto.getDiscountAmount());}return dto;}private void calculateDiscountDetails(Map detailMap, List courses,int totalAmount, int discountAmount) {int times = 0;int remainDiscount = discountAmount;for (OrderCourseDTO course : courses) {// 更新课程已计算数量times++;int discount = 0;// 判断是否是最后一个课程if (times == courses.size()) {// 是最后一个课程,总折扣金额 - 之前所有商品的折扣金额之和discount = remainDiscount;} else {// 计算折扣明细(课程价格在总价中占的比例,乘以总的折扣)discount = discountAmount * course.getPrice() / totalAmount;remainDiscount -= discount;}// 更新折扣明细detailMap.put(course.getId(), discount + detailMap.get(course.getId()));}}private Map> findAvailableCoupon(List coupons, List courses) {Map> map = new HashMap<>(coupons.size());for (Coupon coupon : coupons) {// 1.找出优惠券的可用的课程List availableCourses = courses;if (coupon.getSpecific()) {// 1.1.限定了范围,查询券的可用范围List scopes = scopeService.lambdaQuery().eq(CouponScope::getCouponId, coupon.getId()).list();// 1.2.获取范围对应的分类idSet scopeIds = scopes.stream().map(CouponScope::getBizId).collect(Collectors.toSet());// 1.3.筛选课程availableCourses = courses.stream().filter(c -> scopeIds.contains(c.getCateId())).collect(Collectors.toList());}if (CollUtils.isEmpty(availableCourses)) {// 没有任何可用课程,抛弃continue;}// 2.计算课程总价int totalAmount = availableCourses.stream().mapToInt(OrderCourseDTO::getPrice).sum();// 3.判断是否可用Discount discount = DiscountStrategy.getDiscount(coupon.getDiscountType());if (discount.canUse(totalAmount, coupon)) {map.put(coupon, availableCourses);}}return map;}
}

三、练习(TODO)

参考博客:天机学堂day12所有接口及成功测试结果(含答案练习、个人记录、仅供参考)-CSDN博客

3.1.根据券方案计算订单优惠明细

3.2.核销优惠券

3.3.退还优惠券

3.4.查询优惠券

                        都看到这了,给文涛点个赞支持一下呗!

                                        你的‘赞’,是给与文涛最大的动力鸭

                                        有问题,可以评论区大家一起讨论

                                        后续会在此更新,相关问题及解决方案

http://www.jsqmd.com/news/129257/

相关文章:

  • 2025年口碑好的钱币品牌推荐榜 - 品牌宣传支持者
  • 实现逆变器Modbus转IEC 104协议接入电力监控系统项目案例 - vfbox
  • 豆包GEO优化新生态:精准流量争夺下的优质服务商优选指南 - 品牌推荐排行榜
  • Open-AutoGLM浏览器助手实战指南:5大核心功能让你秒变自动化高手
  • 西安不锈钢水箱制造商厂家推荐,看哪家实力凸显? - 工业推荐榜
  • 2025年专业食品加工降温设备厂家排名,渔船/大型工厂降温设备品牌推荐 - myqiye
  • CAP定理
  • 河南种子会的参与度高吗?对农业增产有作用吗? - 工业推荐榜
  • Qt与MySQL连接过程中出现“QSqlDatabase: QMYSQL driver not loaded”问题
  • 收藏!从7年Java到AI应用工程师:裸辞All in AI一年,我的蜕变全记录
  • Open-AutoGLM开源地址泄露?(内部人员透露的真实仓库链接已验证)
  • 2025年年终产品管理平台推荐:核心技术能力实测与五大品牌对比研究 - 十大品牌推荐
  • 【收藏】从零入门大模型学习:程序员必看的系统路线,轻松打通从理论到实战
  • 2025留学申诉十大机构排名:专业力量护航学业危机破解 - 品牌评测分析
  • 如何快速搭建Open-AutoGLM测试 pipeline?5分钟部署方案曝光
  • 企业数据集成架构深度解析:如何选择高可用的API对接厂商与实战指南
  • H100 真的被封印了吗?我用 vLLM+FP8 把吞吐拉爆了
  • 2025绿色建材企业TOP5权威推荐:博康特石膏基轻质节能快装模块 - mypinpai
  • Proteus汉化助力电子信息类专业教学创新
  • 北京合生利源的管理水平怎么样?技术实力怎样? - 工业品牌热点
  • 常用代码调试记录
  • 2025.12 模拟赛日志
  • 政府信息公开查询系统改造——采用anything-llm提升服务体验
  • 2025重庆口碑好的权威短视频拍摄运营公司排行榜 - 朴素的承诺
  • 大模型(LLM)从入门到精通:测试人的技术跃迁指南
  • LangFlow支持本地与云端双模式运行
  • 第17届中国高端家电趋势发布暨红顶奖颁奖盛典在京开幕,赋能行业高质量发展
  • 當了 15 年 C++ 導師,我發現自己教的類型觀念全是錯的
  • 别墅防水防潮指南:五大标杆产品深度解析与左工防潮解决方案 - 品牌评测分析
  • 读人机沟通法则:理解数字世界的设计与形成04机器是不完整的