項目中優惠券計算邏輯全解析(處理高并發)

其實這個部分的代碼已經完成一陣子了,但是想了一下決定還是整理一下這部分的代碼,因為最開始做的時候業務邏輯還是感覺挺有難度的

整體流程概述

優惠方案計算主要在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…