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

整体流程概述

优惠方案计算主要在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.pswp.cn/news/919811.shtml
繁体地址,请注明出处:http://hk.pswp.cn/news/919811.shtml
英文地址,请注明出处:http://en.pswp.cn/news/919811.shtml

如若内容造成侵权/违法违规/事实不符,请联系英文站点网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

支持电脑课程、游戏、会议、网课、直播录屏 多场景全能录屏工具

白鲨录屏大师&#xff1a;支持电脑课程、游戏、会议、网课、直播录屏 多场景全能录屏工具&#xff0c;轻松捕捉每一刻精彩 在数字化学习、娱乐与办公场景中&#xff0c;高质量的录屏需求日益增长。无论是课程内容的留存、游戏高光的记录&#xff0c;还是会议要点的复盘、网课知…

LeetCode算法日记 - Day 20: 两整数之和、只出现一次的数字II

目录 1. 两数之和 1.1 题目解析 1.2 解法 1.3 代码实现 2. 只出现一次的数字II 2.1 题目解析 2.2 解法 2.3 代码实现 1. 两数之和 371. 两整数之和 - 力扣&#xff08;LeetCode&#xff09; 给你两个整数 a 和 b &#xff0c;不使用 运算符 和 - &#xff0c;计算并…

Spring AI 快速接入 DeepSeek 大模型

Spring AI 快速接入 DeepSeek 大模型 文章目录Spring AI 快速接入 DeepSeek 大模型Spring AI 框架概述核心特性适用场景官网与资源AI 提供商与模型类型模型类型&#xff08;Model Type&#xff09;AI提供商&#xff08;Provider&#xff09;两者的关系Spring AI 框架支持哪些 A…

jQuery 知识点复习总览

文章目录jQuery 知识点复习总览一、jQuery 基础1. jQuery 简介2. jQuery 引入3. jQuery 核心函数二、选择器1. 基本选择器2. 层级选择器3. 过滤选择器4. 表单选择器三、DOM 操作1. 内容操作2. 属性操作3. CSS 操作4. 元素操作四、事件处理1. 事件绑定2. 事件对象3. 自定义事件五…

博客系统接口自动化练习

框架图&#xff1a; 详细代码地址&#xff1a;gitee仓库 博客系统接口自动化文档请看文章顶部。

智慧矿山误报率↓83%!陌讯多模态融合算法在矿用设备监控的落地优化

原创声明&#xff1a;本文为原创技术解析文章&#xff0c;核心技术参数与架构设计引用自 “陌讯技术白皮书&#xff08;智慧矿山专项版&#xff09;”&#xff0c;算法部署相关资源适配参考aishop.mosisson.com平台的陌讯视觉算法专项适配包&#xff0c;禁止未经授权的转载与二…

Laravel 使用阿里云OSS S3 协议文件上传

1. 安装 S3 软件包 composer require league/flysystem-aws-s3-v3 "^3.0" --with-all-dependencies2. 配置.env 以阿里云 OSS 地域华东2 上海为例: FILESYSTEM_DISKs3 //设置默认上传到S3AWS_ACCESS_KEY_ID***…

UVM一些不常用的功能

uvm_coreservice_t是什么AI&#xff1a;在 UVM&#xff08;Universal Verification Methodology&#xff09;中&#xff0c;uvm_coreservice_t 是一个核心服务类&#xff0c;它扮演着UVM 框架内部核心服务的 “管理者” 和 “统一入口” 的角色。其主要作用是封装并提供对 UVM …

怎么确定mongodb是不是链接上了?

现有mongosh链接了MongoDB,里面能操作,但是想python进行链接,因为代码需要,现在测试下链接成功了没有。如下: 要确认你的 MongoDB 连接是否成功,可以通过以下方法检查: 1. 使用 list_database_names 方法【测试成功】 python import asyncioasync def test_connecti…

Unity 二进制读写小框架

文章目录前言框架获取与集成使用方法基本配置自动生成序列化方法实战示例技术原理与优势二进制序列化的优势SJBinary的设计特点最佳实践建议适用场景总结前言 在Unity开发过程中&#xff0c;与后台交互时经常需要处理大型数据文件。当遇到一个近2MB的本地JSON文件需要解析为对…

​Kubernetes 详解:云原生时代的容器编排与管理

一 Kubernetes 简介及部署方法 1.1 应用部署方式演变 在部署应用程序的方式上&#xff0c;主要经历了三个阶段&#xff1a; 传统部署&#xff1a;互联网早期&#xff0c;会直接将应用程序部署在物理机上 优点&#xff1a;简单&#xff0c;不需要其它技术的参与 缺点&#xf…

Kotlin 中的枚举类 Enum Class

枚举类在 Kotlin 中是非常强大和灵活的工具,可以用于表示一组固定的常量,并且可以包含属性、方法、构造函数和伴生对象。它们在处理状态、选项等场景中非常有用。 1、枚举类的定义 枚举类用于创建具有一组数量有限的可能值的类型。 枚举的每个可能值都称为“枚举常量”。每个…

集成电路学习:什么是K-NN最近邻算法

K-NN:最近邻算法 K-NN,即K-最近邻算法(K-Nearest Neighbor algorithm),是一种基本的监督学习算法,广泛应用于分类和回归问题中。以下是对K-NN算法的详细解析: 一、K-NN算法的基本原理 1、K-NN算法的核心思想是: 对于一个新的数据点,算法会在训练数据集中找到与…

2025最新版mgg格式转MP3,mflac转mp3,mgg格式如何转mp3?

注&#xff1a;需要使用旧版客户端&#xff0c;并需要禁用更新。使用说明内有链接打开软件&#xff0c;可以选择将待转换的歌曲拖入&#xff1b;或者点击添加将mgg或者mflac歌曲拖入点击开始转换等待一会就转换完成&#xff0c;默认转换后的歌曲存在桌面的【转换成功】的文件夹…

嵌入式学习day34-网络-tcp/udp

day33练习&#xff1a;客户端 与 服务器实现一个点对点聊天tcp客户端clifd socketconnect//收 --父进程 //发 --子进程 tcp服务器 listenfd socketbindlistenconnfd accept()//收 -- 父进程 //发 -- 子进程client.c#include "../head.h"int res_fd[1]; // 只需要存…

零知开源——基于STM32F103RBT6与ADXL362三轴加速度计的体感迷宫游戏设计与实现

✔零知IDE 是一个真正属于国人自己的开源软件平台&#xff0c;在开发效率上超越了Arduino平台并且更加容易上手&#xff0c;大大降低了开发难度。零知开源在软件方面提供了完整的学习教程和丰富示例代码&#xff0c;让不懂程序的工程师也能非常轻而易举的搭建电路来创作产品&am…

《Linux 网络编程一:网络编程导论及UDP 服务器的创建与数据接收》

Linux下的网络编程1. 目的实现不同主机之间进程的通信。2. 问题主机之间在物理层面必须互联互通。进程之间在软件层面必须互联互通。IP地址&#xff1a;计算机的软件地址&#xff0c;用于标识计算机设备。MAC地址&#xff1a;计算机的硬件地址&#xff08;固定&#xff09;。网…

排序(数据结构)

比较排序 插入排序&#xff08;斗地主摸牌就是一个插入排序&#xff09; 单纯的插入排序也叫直接插入排序 时间复杂度&#xff1a; 最好O(n)最坏O(n^2) 过程 先写单趟&#xff0c;再写整体 依次比较&#xff0c;如果大于就往后挪动&#xff0c;否则就退出循环&#xff0c;插入数…

【C++组件】Elasticsearch 安装及使用

&#x1f308; 个人主页&#xff1a;Zfox_ &#x1f525; 系列专栏&#xff1a;C框架/库 目录&#x1f525; 介绍 &#x1f525; ES 安装 &#x1f98b; 安装 kibana&#x1f98b; ES 客户端的安装&#x1f525; ES 核心概念 &#x1f98b; 索引&#xff08;Index&#xff09;&…

项目:电动车报警器

1.项目需求 点击遥控器A按键&#xff0c;系统进入警戒模式&#xff0c;一旦检测到震动(小偷偷车)&#xff0c;则喇叭发出声响报警&#xff0c;吓退小偷。 点击遥控器B按键&#xff0c;系统退出警戒模式&#xff0c;再怎么摇晃系统都不会报警&#xff0c;否则系统一直发出尖叫&a…