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

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

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

整体流程概述

优惠方案计算主要在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.dtcms.com/a/344028.html

相关文章:

  • 河南萌新联赛2025第(六)场:郑州大学(补题)
  • Unity UnityWebRequest高级操作
  • Masked Language Model 如何重塑大模型的预训练
  • 如何轻松永久删除 Android 手机上的短信
  • 如何从根源上理解并解决前端的CORS跨域问题
  • apt update Ign and 404 Not Found
  • docker cuda版安装 dockercuda版安装
  • 哪款云手机比较好用呢?
  • 链式法则解释上游梯度应用
  • 《Windows Server 2022》 [2025年8月版 ] [官方IOS] 下载
  • 设计模式:抽象工厂模式
  • DeepSeek辅助编写的测试xlsx文件写入性能的程序
  • 多线程下为什么用ConcurrentHashMap而不是HashMap
  • Python万里长征6(非教程)pandas筛选数据三基础、三核心、三高级
  • Kafka 为什么具有高吞吐量的特性?
  • C# 浮点数与定点数详细解析
  • 邀请函 | 2025达索系统高峰论坛,跨界融合定义未来制造
  • SamOutVXP:革命性轻量级语言模型,突破传统推理限制
  • 不同类型代理 IP 在爬虫场景下的表现对比
  • 苹果紧急修复ImageIO零日漏洞CVE-2025-43300,已被在野利用
  • 开源AI编程工具Kilo Code的深度分析:与Cline和Roo Code的全面对比
  • QT之QSS常用颜色总结
  • 【黑客技术零基础入门】计算机网络---子网划分、子网掩码和网关(非常详细)零基础入门到精通,收藏这一篇就够了
  • 【每天一个知识点】AIOps 与自动化管理
  • 二、高可用架构(Nginx + Keepalived + MySQL 主从)
  • 集成算法(聚类)
  • Vue生命周期以及自定义钩子和路由
  • Manus AI 与多语言手写识别技术全解析
  • c++最新进展
  • linux下top命令分析内存不足vs负载过高