天机学堂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 {
@Override
public List findDiscountSolution(List orderCourses) {
//TODO
return 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;
@Override
public 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.获取范围对应的分类id
Set 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;
@Override
public 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.获取范围对应的分类id
Set 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;
@Override
public 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.初始化DTO
CouponDiscountDTO 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.获取范围对应的分类id
Set 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 {
@Bean
public 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
@Bean
public 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;
@Override
public 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.初始化DTO
CouponDiscountDTO 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.获取范围对应的分类id
Set 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;
@Override
public 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.初始化DTO
CouponDiscountDTO 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.获取范围对应的分类id
Set 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.查询优惠券
都看到这了,给文涛点个赞支持一下呗!
你的‘赞’,是给与文涛最大的动力鸭
有问题,可以评论区大家一起讨论
后续会在此更新,相关问题及解决方案

浙公网安备 33010602011771号