其實這個部分的代碼已經完成一陣子了,但是想了一下決定還是整理一下這部分的代碼,因為最開始做的時候業務邏輯還是感覺挺有難度的
整體流程概述
優惠方案計算主要在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工具),這些技術的詳細解釋會在后面的文章中給出