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

项目中优惠券计算逻辑全解析(处理高并发)

其实这个部分的代码已经完成一阵子了,但是想了一下决定还是整理一下这部分的代码,因为最开始做的时候业务逻辑还是感觉挺有难度的

整体流程概述

优惠方案计算主要在DiscountServiceImpl类的findDiscountSolution方法中实现。整个计算过程可以分为以下五个步骤:
①查询用户可用优惠券
②初步筛选可用优惠券
③细化筛选并生成优惠券组合
④并行计算各种组合的优惠明细
⑥筛选最优方案
下面我们来逐一分析每个步骤的具体实现

第一步:查询用户可用优惠券


首先,系统需要获取当前用户持有的所有优惠券:

Long user = UserContext.getUser();
List<Coupon> coupons = userCouponMapper.queryMyCoupons(user);

这一步通过用户上下文获取当前用户ID,然后查询该用户持有的所有未过期、未使用的优惠券。


第二步:初步筛选可用优惠券


初步筛选是基于订单总价进行的。系统会计算订单中所有课程的总价,然后筛选出满足使用门槛的优惠券:

// 计算订单总价
int sum = orderCourses.stream().mapToInt(OrderCourseDTO::getPrice).sum();// 筛选可用券
List<Coupon> availableCoupons = coupons.stream().filter(c -> DiscountStrategy.getDiscount(c.getDiscountType()).canUse(sum, c)).collect(Collectors.toList());

这里使用了策略模式,根据优惠券类型获取对应的折扣计算策略,然后判断该优惠券是否可以在当前订单总价下使用。


第三步:细化筛选并生成优惠券组合


这一步是最复杂的,它包含两个子步骤:


3.1 细化筛选(找出每个优惠券的可用课程)


对于每张优惠券,需要根据其限定范围筛选出订单中可用的课程,并判断这些课程的总价是否满足优惠券使用条件:

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


通过排列组合算法生成所有可能的优惠券组合,并添加单张优惠券的方案:

availableCoupons = new ArrayList<>(availableCouponMap.keySet());
List<List<Coupon>> solutions = PermuteUtil.permute(availableCoupons);
// 添加单券的方案
for (Coupon c : availableCoupons) {solutions.add(List.of(c));
}

第四步:并行计算各种组合的优惠明细


对于生成的每种优惠券组合方案,系统会并行计算其优惠金额。这里使用了CompletableFuture和CountDownLatch来实现异步并行计算:

List<CouponDiscountDTO> list = Collections.synchronizedList(new ArrayList<>(solutions.size()));
// 定义闭锁
CountDownLatch latch = new CountDownLatch(solutions.size());
for (List<Coupon> solution : solutions) {// 异步计算CompletableFuture.supplyAsync(() -> calculateSolutionDiscount(availableCouponMap, orderCourses, solution),discountSolutionExecutor).thenAccept(dto -> {// 提交任务结果list.add(dto);latch.countDown();});
}
// 等待运算结束
try {latch.await(1, TimeUnit.SECONDS);
} catch (InterruptedException e) {log.error("优惠方案计算被中断,{}", e.getMessage());
}

其中,calculateSolutionDiscount方法负责具体计算一个组合方案的优惠明细:

private CouponDiscountDTO calculateSolutionDiscount(Map<Coupon, List<OrderCourseDTO>> couponMap, List<OrderCourseDTO> courses, List<Coupon> solution) {// 初始化DTOCouponDiscountDTO dto = new CouponDiscountDTO();// 初始化折扣明细的映射Map<Long, Integer> detailMap = courses.stream().collect(Collectors.toMap(OrderCourseDTO::getId, oc -> 0));// 计算折扣for (Coupon coupon : solution) {// 获取优惠券限定范围对应的课程List<OrderCourseDTO> availableCourses = couponMap.get(coupon);// 计算课程总价(课程原价 - 折扣明细)int totalAmount = availableCourses.stream().mapToInt(oc -> oc.getPrice() - detailMap.get(oc.getId())).sum();// 判断是否可用Discount discount = DiscountStrategy.getDiscount(coupon.getDiscountType());if (!discount.canUse(totalAmount, coupon)) {// 券不可用,跳过continue;}// 计算优惠金额int discountAmount = discount.calculateDiscount(totalAmount, coupon);// 计算优惠明细calculateDiscountDetails(detailMap, availableCourses, totalAmount, discountAmount);// 更新DTO数据dto.getIds().add(coupon.getCreater());dto.getRules().add(discount.getRule(coupon));dto.setDiscountAmount(discountAmount + dto.getDiscountAmount());}return dto;
}

优惠明细的计算通过calculateDiscountDetails方法实现,它将总优惠金额按比例分摊到各个课程上:

private void calculateDiscountDetails(Map<Long, Integer> detailMap, List<OrderCourseDTO> 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 List<CouponDiscountDTO> findBestSolution(List<CouponDiscountDTO> list) {// 准备Map记录最优解Map<String, CouponDiscountDTO> moreDiscountMap = new HashMap<>();Map<Integer, CouponDiscountDTO> lessCouponMap = new HashMap<>();// 遍历,筛选最优解for (CouponDiscountDTO solution : list) {// 计算当前方案的id组合String ids = solution.getIds().stream().sorted(Long::compare).map(String::valueOf).collect(Collectors.joining(","));// 比较用券相同时,优惠金额是否最大CouponDiscountDTO best = moreDiscountMap.get(ids);if (best != null && best.getDiscountAmount() >= solution.getDiscountAmount()) {// 当前方案优惠金额少,跳过continue;}// 比较金额相同时,用券数量是否最少best = lessCouponMap.get(solution.getDiscountAmount());int size = solution.getIds().size();if (size > 1 && best != null && best.getIds().size() <= size) {// 当前方案用券更多,放弃continue;}// 更新最优解moreDiscountMap.put(ids, solution);lessCouponMap.put(solution.getDiscountAmount(), solution);}// 求交集Collection<CouponDiscountDTO> bestSolutions = CollUtils.intersection(moreDiscountMap.values(), lessCouponMap.values());// 排序,按优惠金额降序return bestSolutions.stream().sorted(Comparator.comparingInt(CouponDiscountDTO::getDiscountAmount).reversed()).collect(Collectors.toList());
}

总结


优惠方案计算通过以上五个步骤,能够为用户推荐最优化的优惠券使用方案。整个过程考虑了以下关键因素:
①优惠券的适用范围和使用门槛
②多张优惠券的组合使用
③并行计算提高性能
④优惠金额在订单商品间的合理分摊
⑤最优方案的选择策略
这种设计既保证了计算结果的准确性,又通过并行计算提高了性能,为用户提供了良好的购物体验,最后对于这其中所用到的一些新的技术,如(策略模式,CountdownLatch工具和CompletableFuture工具),这些技术的详细解释会在后面的文章中给出

http://www.xdnf.cn/news/18441.html

相关文章:

  • Unreal Engine UStaticMeshComponent
  • JUC之CompletionService
  • DFS序与树链剖分入门
  • 开发避坑指南(35):mybaits if标签test条件判断等号=解析异常解决方案
  • 文件系统层面的可用块数量可用空间和比例
  • AI重塑职业教育:个性化学习计划提效率、VR实操模拟强技能,对接就业新路径
  • 拿到手一个前端项目,应该如何启动
  • 开发避坑指南(34):mysql深度分页查询优化方案
  • Ubuntu解决makefile交叉编译的问题
  • Android Jetpack | Hilt
  • 机器人爆发、汽车换代,速腾聚创开始讲新故事
  • WindowsAPI|每天了解几个winAPI接口之网络配置相关文档Iphlpapi.h详细分析八
  • 【数据结构】选择排序:直接选择与堆排序详解
  • 前端项目打包+自动压缩打包文件+自动上传部署远程服务器
  • 为什么需要关注Flink并行度?
  • 【C#】观察者模式 + UI 线程调度、委托讲解
  • 大学校园安消一体化平台——多警合一实现智能联动与网格化管理
  • Redis 678
  • Hyperledger Fabric官方中文教程-改进笔记(十四)-向通道中添加组织
  • open webui源码分析7—过滤器
  • 获取后台返回的错误码
  • Linux822 shell:expect 批量
  • 车辆方向数据集 - 物体检测
  • 作品集PDF又大又卡?我用InDesign+Acrobat AI构建轻量化交互式文档工作流
  • 【LeetCode每日一题】238. 除自身以外数组的乘积
  • 【链表 - LeetCode】2. 两数相加
  • 服务器与客户端
  • 零基础从头教学Linux(Day 18)
  • 北斗导航 | 基于MCMC粒子滤波的接收机自主完好性监测(RAIM)算法(附matlab代码)
  • 【Linux我做主】细说进程地址空间