package com.ruoyi.system.service.impl; import java.math.BigDecimal; import java.math.RoundingMode; import java.util.ArrayList; import java.util.Date; import java.util.HashMap; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; import java.util.stream.Collectors; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; import com.ruoyi.system.domain.PosFood; import com.ruoyi.system.domain.PosOrder; import com.ruoyi.system.domain.PromotionActivity; import com.ruoyi.system.domain.PromotionActivityRule; import com.ruoyi.system.domain.PromotionCouponBatch; import com.ruoyi.system.domain.PromotionCouponRule; import com.ruoyi.system.domain.PromotionUserCoupon; import com.ruoyi.system.dto.PromotionCalcRequest; import com.ruoyi.system.dto.PromotionCalcResponse; import com.ruoyi.system.dto.PromotionCalcResponse.AvailableCoupon; import com.ruoyi.system.dto.PromotionCalcResponse.LineItem; import com.ruoyi.system.dto.PromotionCalcResponse.MatchedRule; import com.ruoyi.system.dto.PromotionCalcResponse.PathResult; import com.ruoyi.system.dto.PromotionCalcResponse.PromotionDetail; import com.ruoyi.system.mapper.PosFoodMapper; import com.ruoyi.system.mapper.PosOrderMapper; import com.ruoyi.system.mapper.PromotionActivityMapper; import com.ruoyi.system.mapper.PromotionActivityRuleMapper; import com.ruoyi.system.mapper.PromotionCouponBatchMapper; import com.ruoyi.system.mapper.PromotionCouponRuleMapper; import com.ruoyi.system.mapper.PromotionUserCouponMapper; import com.ruoyi.system.service.IPromotionCalcService; /** * 促销算价Service实现 * * 两路算价:Path A(折扣/第二份半价)vs Path B(满减),取最优路径。 * 叠加新客立减、优惠券后得出最终金额。 */ @Service public class PromotionCalcServiceImpl implements IPromotionCalcService { private static final BigDecimal MIN_AMOUNT = new BigDecimal("1"); @Autowired private PromotionActivityMapper activityMapper; @Autowired private PromotionActivityRuleMapper activityRuleMapper; @Autowired private PromotionCouponBatchMapper couponBatchMapper; @Autowired private PromotionCouponRuleMapper couponRuleMapper; @Autowired private PromotionUserCouponMapper userCouponMapper; @Autowired private PosFoodMapper posFoodMapper; @Autowired private PosOrderMapper posOrderMapper; @Override public PromotionCalcResponse calculate(PromotionCalcRequest request, Long userId) { Long storeId = request.getStoreId(); Long couponId = request.getCouponId(); String forcePath = request.getForcePath(); // 将 DTO items 转为内部 Map 格式 List> items = new ArrayList<>(); if (request.getItems() != null) { for (PromotionCalcRequest.CartItem ci : request.getItems()) { Map m = new LinkedHashMap<>(); m.put("productId", ci.getProductId()); m.put("quantity", ci.getQuantity()); items.add(m); } } // ---- 1. 获取商品真实价格,计算 originalAmount ---- Map priceMap = new HashMap<>(); Map nameMap = new HashMap<>(); for (Map item : items) { Long productId = toLong(item.get("productId")); if (productId != null && !priceMap.containsKey(productId)) { PosFood food = posFoodMapper.selectById(productId); if (food != null) { priceMap.put(productId, food.getPrice()); nameMap.put(productId, food.getName()); } } } BigDecimal originalAmount = BigDecimal.ZERO; List> itemsWithPrice = new ArrayList<>(); for (Map item : items) { Long productId = toLong(item.get("productId")); int quantity = toInt(item.get("quantity")); BigDecimal unitPrice = priceMap.getOrDefault(productId, BigDecimal.ZERO); originalAmount = originalAmount.add(unitPrice.multiply(BigDecimal.valueOf(quantity))); Map ip = new LinkedHashMap<>(); ip.put("productId", productId); ip.put("quantity", quantity); ip.put("unitPrice", unitPrice); ip.put("name", nameMap.getOrDefault(productId, "")); itemsWithPrice.add(ip); } originalAmount = originalAmount.setScale(0, RoundingMode.HALF_UP); // ---- 2. 查询门店生效的促销活动 ---- List activeActivities = activityMapper.selectActiveByStoreId(storeId); List activeRules = activityRuleMapper.selectActiveRulesByStoreId(storeId); Map activityMap = activeActivities.stream() .collect(Collectors.toMap(PromotionActivity::getId, a -> a, (a, b) -> a)); Map> rulesByActivity = activeRules.stream() .collect(Collectors.groupingBy(PromotionActivityRule::getActivityId)); Map> rulesByProduct = activeRules.stream() .filter(r -> r.getProductId() != null) .collect(Collectors.groupingBy(PromotionActivityRule::getProductId)); // ---- 3. PATH A: 折扣(type=2) / 第二份半价(type=3) ---- Map pathAResult = calculatePathA(itemsWithPrice, activeActivities, rulesByActivity, rulesByProduct, activityMap); // ---- 4. PATH B: 满减(type=1) ---- Map pathBResult = calculatePathB(itemsWithPrice, originalAmount, activeActivities, rulesByActivity, activityMap); // ---- 5. 比较两条路径 ---- BigDecimal subtotalA = (BigDecimal) pathAResult.get("subtotal"); BigDecimal subtotalB = (BigDecimal) pathBResult.get("subtotal"); String optimalPath; if (forcePath != null && ("A".equalsIgnoreCase(forcePath) || "B".equalsIgnoreCase(forcePath))) { optimalPath = forcePath.toUpperCase(); } else { optimalPath = subtotalA.compareTo(subtotalB) < 0 ? "A" : "B"; } BigDecimal afterPromotion = "A".equals(optimalPath) ? subtotalA : subtotalB; BigDecimal promotionReduce = originalAmount.subtract(afterPromotion); // ---- 6. 新客立减(type=4) ---- BigDecimal newCustomerReduce = BigDecimal.ZERO; if (userId != null) { LambdaQueryWrapper orderWrapper = new LambdaQueryWrapper<>(); orderWrapper.eq(PosOrder::getUserId, userId) .eq(PosOrder::getMdId, storeId) .eq(PosOrder::getPayStatus, 1L); Long paidCount = posOrderMapper.selectCount(orderWrapper); if (paidCount == 0) { for (PromotionActivity act : activeActivities) { if (act.getType() != null && act.getType() == 4) { List ncRules = rulesByActivity.get(act.getId()); if (ncRules != null && !ncRules.isEmpty()) { BigDecimal reduce = ncRules.get(0).getReduceAmount(); if (reduce != null && reduce.compareTo(BigDecimal.ZERO) > 0) { newCustomerReduce = reduce; } } break; } } } } // ---- 7. 优惠券 ---- BigDecimal couponReduce = BigDecimal.ZERO; String couponName = null; Boolean couponConflict = null; String conflictNote = null; PromotionCouponBatch batch = null; // 商品券生效时, 记录参与优惠的商品明细(名称/原价/优惠后价) List> couponProductItems = null; if (couponId != null) { PromotionUserCoupon userCoupon = userCouponMapper.selectById(couponId); if (userCoupon != null && userCoupon.getUserId().equals(userId) && userCoupon.getStatus() != null && userCoupon.getStatus() == 0) { if (userCoupon.getExpireTime() != null && userCoupon.getExpireTime().before(new Date())) { // 已过期,不处理 } else { batch = couponBatchMapper.selectById(userCoupon.getBatchId()); PromotionCouponRule couponRule = couponRuleMapper.selectRuleByBatchId(userCoupon.getBatchId()); if (batch != null && couponRule != null) { couponName = batch.getName(); int isMutex = couponRule.getIsMutex() != null ? couponRule.getIsMutex() : 0; if (isMutex == 1 && promotionReduce.compareTo(BigDecimal.ZERO) > 0) { couponConflict = true; conflictNote = "互斥券不可与满减/折扣叠加,选择此券将取消促销优惠"; CouponCalcResult mutexResult = calcCouponReduce(couponRule, batch, itemsWithPrice, originalAmount, priceMap, nameMap); couponReduce = mutexResult.reduce; couponProductItems = mutexResult.items; afterPromotion = originalAmount; promotionReduce = BigDecimal.ZERO; newCustomerReduce = BigDecimal.ZERO; } else { BigDecimal baseForCoupon = afterPromotion.subtract(newCustomerReduce); baseForCoupon = baseForCoupon.max(BigDecimal.ZERO); if (couponRule.getThreshold() == null || baseForCoupon.compareTo(couponRule.getThreshold()) >= 0) { if (batch.getCouponType() != null && batch.getCouponType() == 2 && couponRule.getProductId() != null && "A".equals(optimalPath)) { List productRules = rulesByProduct.get(couponRule.getProductId()); if (productRules != null) { boolean inDiscount = productRules.stream().anyMatch(r -> { PromotionActivity act = activityMap.get(r.getActivityId()); return act != null && (act.getType() == 2 || act.getType() == 3); }); if (inDiscount) { couponConflict = true; conflictNote = "商品券对应的商品在折扣活动中,不可与Path A叠加"; } } } if (couponConflict == null) { CouponCalcResult normalResult = calcCouponReduce(couponRule, batch, itemsWithPrice, baseForCoupon, priceMap, nameMap); couponReduce = normalResult.reduce; couponProductItems = normalResult.items; } } } } } } } // ---- 8. 最终金额 ---- BigDecimal finalAmount = afterPromotion.subtract(newCustomerReduce).subtract(couponReduce); finalAmount = finalAmount.max(MIN_AMOUNT).setScale(0, RoundingMode.HALF_UP); // ---- 9. 构建响应DTO ---- PromotionCalcResponse response = new PromotionCalcResponse(); response.setOriginalAmount(originalAmount); response.setPathA(toPathResult(pathAResult)); response.setPathB(toPathResult(pathBResult)); response.setOptimalPath(optimalPath); response.setFinalAmount(finalAmount); if (newCustomerReduce.compareTo(BigDecimal.ZERO) > 0) { response.setNewCustomerReduce(newCustomerReduce); } if (couponId != null) { response.setCouponId(couponId); } if (couponName != null) { response.setCouponName(couponName); } if (couponReduce.compareTo(BigDecimal.ZERO) > 0) { response.setCouponReduce(couponReduce); } if (couponConflict != null) { response.setCouponConflict(couponConflict); } if (conflictNote != null) { response.setConflictNote(conflictNote); } // 设置优惠券批次ID(用于下单快照写入) if (couponId != null && batch != null) { response.setCouponBatchId(batch.getId()); } // 商品券: 返回参与优惠的商品明细(名称/原价/优惠后价) if (couponProductItems != null && !couponProductItems.isEmpty()) { response.setCouponProductItems(couponProductItems.stream() .map(this::toLineItem).collect(Collectors.toList())); } // ---- 10. 明细列表 ---- List details = new ArrayList<>(); if (promotionReduce.compareTo(BigDecimal.ZERO) > 0) { PromotionDetail detail = new PromotionDetail(); detail.setType("promotion"); if ("A".equals(optimalPath)) { detail.setSubType((Integer) pathAResult.get("appliedType")); detail.setName((String) pathAResult.get("label")); // 找到对应类型的促销活动ID Integer appliedType = (Integer) pathAResult.get("appliedType"); for (PromotionActivity act : activeActivities) { if (act.getType() != null && act.getType().equals(appliedType)) { detail.setRefId(act.getId()); break; } } } else { detail.setSubType(1); detail.setName((String) pathBResult.get("label")); // 从满减匹配规则中获取活动ID @SuppressWarnings("unchecked") Map matchedRule = (Map) pathBResult.get("matchedRule"); if (matchedRule != null) { detail.setRefId(toLong(matchedRule.get("activityId"))); } } detail.setReduce(promotionReduce); details.add(detail); } if (newCustomerReduce.compareTo(BigDecimal.ZERO) > 0) { PromotionDetail detail = new PromotionDetail(); detail.setType("promotion"); detail.setSubType(4); detail.setName("新客立减"); detail.setReduce(newCustomerReduce); // 找到新客立减活动ID for (PromotionActivity act : activeActivities) { if (act.getType() != null && act.getType() == 4) { detail.setRefId(act.getId()); break; } } details.add(detail); } if (couponReduce.compareTo(BigDecimal.ZERO) > 0) { PromotionDetail detail = new PromotionDetail(); detail.setType("coupon"); detail.setSubType(0); detail.setName(couponName); detail.setReduce(couponReduce); // 优惠券的refId设为券批次ID if (batch != null) { detail.setRefId(batch.getId()); } details.add(detail); } response.setDetails(details); // ---- 11. 可用优惠券列表 ---- List> availableCouponMaps = buildAvailableCoupons(storeId, userId, originalAmount, afterPromotion, promotionReduce, itemsWithPrice, rulesByProduct, activityMap, priceMap, nameMap); response.setAvailableCoupons(availableCouponMaps.stream() .map(this::toAvailableCoupon) .collect(Collectors.toList())); return response; } // ==================== DTO转换方法 ==================== private PathResult toPathResult(Map pathMap) { PathResult pr = new PathResult(); pr.setLabel((String) pathMap.get("label")); pr.setSubtotal((BigDecimal) pathMap.get("subtotal")); if (pathMap.containsKey("appliedType")) { pr.setAppliedType((Integer) pathMap.get("appliedType")); } if (pathMap.containsKey("promotionReduce")) { pr.setPromotionReduce((BigDecimal) pathMap.get("promotionReduce")); } @SuppressWarnings("unchecked") List> itemList = (List>) pathMap.get("items"); if (itemList != null) { pr.setItems(itemList.stream().map(this::toLineItem).collect(Collectors.toList())); } @SuppressWarnings("unchecked") Map matchedRuleMap = (Map) pathMap.get("matchedRule"); if (matchedRuleMap != null) { MatchedRule mr = new MatchedRule(); mr.setId(toLong(matchedRuleMap.get("id"))); mr.setActivityId(toLong(matchedRuleMap.get("activityId"))); mr.setActivityName((String) matchedRuleMap.get("activityName")); mr.setThreshold((BigDecimal) matchedRuleMap.get("threshold")); mr.setReduceAmount((BigDecimal) matchedRuleMap.get("reduceAmount")); pr.setMatchedRule(mr); } return pr; } private LineItem toLineItem(Map itemMap) { LineItem li = new LineItem(); li.setProductId(toLong(itemMap.get("productId"))); li.setQuantity(toInt(itemMap.get("quantity"))); li.setUnitPrice((BigDecimal) itemMap.get("unitPrice")); li.setName((String) itemMap.get("name")); li.setOriginalLineTotal((BigDecimal) itemMap.get("originalLineTotal")); li.setFinalLineTotal((BigDecimal) itemMap.get("finalLineTotal")); li.setDiscountRate((BigDecimal) itemMap.get("discountRate")); li.setHalfPriceApplied((Boolean) itemMap.get("halfPriceApplied")); return li; } private AvailableCoupon toAvailableCoupon(Map couponMap) { AvailableCoupon ac = new AvailableCoupon(); ac.setId(toLong(couponMap.get("id"))); ac.setName((String) couponMap.get("name")); ac.setCouponType((Integer) couponMap.get("couponType")); ac.setIsMutex((Integer) couponMap.get("isMutex")); ac.setThreshold((BigDecimal) couponMap.get("threshold")); ac.setAmount((BigDecimal) couponMap.get("amount")); ac.setDiscountRate((BigDecimal) couponMap.get("discountRate")); ac.setUsable((Boolean) couponMap.get("usable")); ac.setConflictWithPromotion((Boolean) couponMap.get("conflictWithPromotion")); ac.setCouponPreviewReduce((BigDecimal) couponMap.get("couponPreviewReduce")); @SuppressWarnings("unchecked") List> productPreview = (List>) couponMap.get("productPreview"); if (productPreview != null && !productPreview.isEmpty()) { ac.setProductPreview(productPreview.stream().map(this::toLineItem).collect(Collectors.toList())); } ac.setExpireTime((java.util.Date) couponMap.get("expireTime")); return ac; } // ==================== 内部计算方法(保持Map) ==================== /** * PATH A: 折扣(type=2) vs 第二份半价(type=3),取更优 */ private Map calculatePathA(List> itemsWithPrice, List activeActivities, Map> rulesByActivity, Map> rulesByProduct, Map activityMap) { Map pathResult = new LinkedHashMap<>(); pathResult.put("label", "折扣"); pathResult.put("appliedType", 2); Map discountResult = calcDiscount(itemsWithPrice, rulesByProduct, activityMap); Map halfPriceResult = calcHalfPrice(itemsWithPrice, rulesByProduct, activityMap); BigDecimal discountSubtotal = (BigDecimal) discountResult.get("subtotal"); BigDecimal halfPriceSubtotal = (BigDecimal) halfPriceResult.get("subtotal"); if (halfPriceSubtotal.compareTo(discountSubtotal) < 0) { pathResult.put("label", "第二份半价"); pathResult.put("appliedType", 3); pathResult.put("items", halfPriceResult.get("items")); pathResult.put("subtotal", halfPriceSubtotal); pathResult.put("promotionReduce", discountResult.containsKey("originalRef") ? ((BigDecimal) discountResult.get("originalRef")).subtract(halfPriceSubtotal) .setScale(0, RoundingMode.HALF_UP) : BigDecimal.ZERO); } else { pathResult.put("items", discountResult.get("items")); pathResult.put("subtotal", discountSubtotal); pathResult.put("promotionReduce", discountResult.containsKey("originalRef") ? ((BigDecimal) discountResult.get("originalRef")).subtract(discountSubtotal) .setScale(0, RoundingMode.HALF_UP) : BigDecimal.ZERO); } return pathResult; } /** * 折扣计算: 匹配到商品的规则 → unitPrice * discountRate */ private Map calcDiscount(List> itemsWithPrice, Map> rulesByProduct, Map activityMap) { Map result = new LinkedHashMap<>(); List> itemList = new ArrayList<>(); BigDecimal subtotal = BigDecimal.ZERO; BigDecimal originalRef = BigDecimal.ZERO; for (Map item : itemsWithPrice) { Long productId = toLong(item.get("productId")); int quantity = toInt(item.get("quantity")); BigDecimal unitPrice = (BigDecimal) item.get("unitPrice"); BigDecimal lineOriginal = unitPrice.multiply(BigDecimal.valueOf(quantity)); originalRef = originalRef.add(lineOriginal); BigDecimal finalPrice = lineOriginal; PromotionActivityRule matchedRule = null; List productRules = rulesByProduct.get(productId); if (productRules != null) { for (PromotionActivityRule rule : productRules) { PromotionActivity act = activityMap.get(rule.getActivityId()); if (act != null && act.getType() == 2 && rule.getDiscountRate() != null) { if (rule.getMinQuantity() == null || quantity >= rule.getMinQuantity()) { BigDecimal discounted = unitPrice.multiply(rule.getDiscountRate()) .multiply(BigDecimal.valueOf(quantity)) .setScale(0, RoundingMode.HALF_UP); if (discounted.compareTo(finalPrice) < 0) { finalPrice = discounted; matchedRule = rule; } } } } } Map lineItem = new LinkedHashMap<>(); lineItem.put("productId", productId); lineItem.put("quantity", quantity); lineItem.put("unitPrice", unitPrice); lineItem.put("originalLineTotal", lineOriginal.setScale(0, RoundingMode.HALF_UP)); lineItem.put("finalLineTotal", finalPrice.setScale(0, RoundingMode.HALF_UP)); lineItem.put("name", item.get("name")); if (matchedRule != null) { lineItem.put("discountRate", matchedRule.getDiscountRate()); } itemList.add(lineItem); subtotal = subtotal.add(finalPrice); } result.put("items", itemList); result.put("subtotal", subtotal.setScale(0, RoundingMode.HALF_UP)); result.put("originalRef", originalRef.setScale(0, RoundingMode.HALF_UP)); return result; } /** * 第二份半价计算: 每两件中的第二件按50% */ private Map calcHalfPrice(List> itemsWithPrice, Map> rulesByProduct, Map activityMap) { Map result = new LinkedHashMap<>(); List> itemList = new ArrayList<>(); BigDecimal subtotal = BigDecimal.ZERO; BigDecimal originalRef = BigDecimal.ZERO; for (Map item : itemsWithPrice) { Long productId = toLong(item.get("productId")); int quantity = toInt(item.get("quantity")); BigDecimal unitPrice = (BigDecimal) item.get("unitPrice"); BigDecimal lineOriginal = unitPrice.multiply(BigDecimal.valueOf(quantity)); originalRef = originalRef.add(lineOriginal); BigDecimal finalPrice = lineOriginal; PromotionActivityRule matchedRule = null; List productRules = rulesByProduct.get(productId); if (productRules != null) { for (PromotionActivityRule rule : productRules) { PromotionActivity act = activityMap.get(rule.getActivityId()); if (act != null && act.getType() == 3) { if (rule.getMinQuantity() == null || quantity >= rule.getMinQuantity()) { int pairs = quantity / 2; int remainder = quantity % 2; BigDecimal halfPriceTotal = unitPrice.multiply(BigDecimal.valueOf(pairs)) .multiply(new BigDecimal("1.5")) .add(unitPrice.multiply(BigDecimal.valueOf(remainder))) .setScale(0, RoundingMode.HALF_UP); if (halfPriceTotal.compareTo(finalPrice) < 0) { finalPrice = halfPriceTotal; matchedRule = rule; } } } } } Map lineItem = new LinkedHashMap<>(); lineItem.put("productId", productId); lineItem.put("quantity", quantity); lineItem.put("unitPrice", unitPrice); lineItem.put("originalLineTotal", lineOriginal.setScale(0, RoundingMode.HALF_UP)); lineItem.put("finalLineTotal", finalPrice.setScale(0, RoundingMode.HALF_UP)); lineItem.put("name", item.get("name")); if (matchedRule != null) { lineItem.put("halfPriceApplied", true); } itemList.add(lineItem); subtotal = subtotal.add(finalPrice); } result.put("items", itemList); result.put("subtotal", subtotal.setScale(0, RoundingMode.HALF_UP)); result.put("originalRef", originalRef.setScale(0, RoundingMode.HALF_UP)); return result; } /** * PATH B: 满减(type=1) — 找最高匹配门槛的规则 */ private Map calculatePathB(List> itemsWithPrice, BigDecimal originalAmount, List activeActivities, Map> rulesByActivity, Map activityMap) { Map pathResult = new LinkedHashMap<>(); pathResult.put("label", "满减"); List> itemList = new ArrayList<>(); for (Map item : itemsWithPrice) { Map lineItem = new LinkedHashMap<>(); lineItem.put("productId", item.get("productId")); lineItem.put("quantity", item.get("quantity")); lineItem.put("unitPrice", item.get("unitPrice")); BigDecimal lineTotal = ((BigDecimal) item.get("unitPrice")) .multiply(BigDecimal.valueOf(toInt(item.get("quantity")))) .setScale(0, RoundingMode.HALF_UP); lineItem.put("originalLineTotal", lineTotal); lineItem.put("finalLineTotal", lineTotal); lineItem.put("name", item.get("name")); itemList.add(lineItem); } pathResult.put("items", itemList); PromotionActivityRule bestMatch = null; PromotionActivity bestActivity = null; for (PromotionActivity act : activeActivities) { if (act.getType() != null && act.getType() == 1) { List rules = rulesByActivity.get(act.getId()); if (rules != null) { for (PromotionActivityRule rule : rules) { if (rule.getThreshold() != null && rule.getReduceAmount() != null && originalAmount.compareTo(rule.getThreshold()) >= 0) { if (bestMatch == null || rule.getThreshold().compareTo(bestMatch.getThreshold()) > 0) { bestMatch = rule; bestActivity = act; } } } } } } if (bestMatch != null) { BigDecimal subtotal = originalAmount.subtract(bestMatch.getReduceAmount()) .max(MIN_AMOUNT).setScale(0, RoundingMode.HALF_UP); pathResult.put("subtotal", subtotal); pathResult.put("promotionReduce", originalAmount.subtract(subtotal)); Map matchedRuleInfo = new LinkedHashMap<>(); matchedRuleInfo.put("id", bestMatch.getId()); matchedRuleInfo.put("activityId", bestMatch.getActivityId()); matchedRuleInfo.put("threshold", bestMatch.getThreshold()); matchedRuleInfo.put("reduceAmount", bestMatch.getReduceAmount()); if (bestActivity != null) { matchedRuleInfo.put("activityName", bestActivity.getName()); } pathResult.put("matchedRule", matchedRuleInfo); } else { pathResult.put("subtotal", originalAmount); pathResult.put("promotionReduce", BigDecimal.ZERO); } return pathResult; } /** * 券减免计算结果: reduce=减免金额, items=商品券参与的商品明细(仅商品券有) */ private static class CouponCalcResult { BigDecimal reduce = BigDecimal.ZERO; List> items = new ArrayList<>(); } /** * 计算单张券的减免金额; 商品券(couponType=2)同时返回参与商品明细(名称/原价/优惠后价) */ private CouponCalcResult calcCouponReduce(PromotionCouponRule couponRule, PromotionCouponBatch batch, List> itemsWithPrice, BigDecimal baseAmount, Map priceMap, Map nameMap) { CouponCalcResult result = new CouponCalcResult(); if (batch.getCouponType() != null && batch.getCouponType() == 2) { if (couponRule.getProductId() != null) { Long pid = couponRule.getProductId(); BigDecimal productPrice = priceMap.getOrDefault(pid, BigDecimal.ZERO); int quantity = 0; for (Map item : itemsWithPrice) { if (pid.equals(toLong(item.get("productId")))) { quantity = toInt(item.get("quantity")); break; } } BigDecimal totalProductPrice = productPrice.multiply(BigDecimal.valueOf(quantity)); BigDecimal finalLineTotal = totalProductPrice; if (couponRule.getDiscountRate() != null) { BigDecimal reduction = totalProductPrice.multiply(BigDecimal.ONE.subtract(couponRule.getDiscountRate())); result.reduce = reduction.max(BigDecimal.ZERO).min(totalProductPrice) .setScale(0, RoundingMode.HALF_UP); finalLineTotal = totalProductPrice.subtract(result.reduce).max(BigDecimal.ZERO) .setScale(0, RoundingMode.HALF_UP); } else if (couponRule.getAmount() != null) { result.reduce = couponRule.getAmount().min(totalProductPrice) .setScale(0, RoundingMode.HALF_UP); finalLineTotal = totalProductPrice.subtract(result.reduce).max(BigDecimal.ZERO) .setScale(0, RoundingMode.HALF_UP); } // 购物车含该商品才产出明细 if (quantity > 0) { Map lineItem = new LinkedHashMap<>(); lineItem.put("productId", pid); lineItem.put("quantity", quantity); lineItem.put("unitPrice", productPrice); lineItem.put("name", nameMap.getOrDefault(pid, "")); lineItem.put("originalLineTotal", totalProductPrice.setScale(0, RoundingMode.HALF_UP)); lineItem.put("finalLineTotal", finalLineTotal); if (couponRule.getDiscountRate() != null) { lineItem.put("discountRate", couponRule.getDiscountRate()); } result.items.add(lineItem); } } return result; } else { if (couponRule.getAmount() != null) { result.reduce = couponRule.getAmount().min(baseAmount).setScale(0, RoundingMode.HALF_UP); } return result; } } /** * 构建用户可用优惠券列表 */ private List> buildAvailableCoupons(Long storeId, Long userId, BigDecimal originalAmount, BigDecimal afterPromotion, BigDecimal promotionReduce, List> itemsWithPrice, Map> rulesByProduct, Map activityMap, Map priceMap, Map nameMap) { List> available = new ArrayList<>(); if (userId == null) { return available; } LambdaQueryWrapper wrapper = new LambdaQueryWrapper<>(); wrapper.eq(PromotionUserCoupon::getUserId, userId) .eq(PromotionUserCoupon::getStoreId, storeId) .eq(PromotionUserCoupon::getStatus, 0) .gt(PromotionUserCoupon::getExpireTime, new Date()); List userCoupons = userCouponMapper.selectList(wrapper); for (PromotionUserCoupon uc : userCoupons) { PromotionCouponBatch batch = couponBatchMapper.selectById(uc.getBatchId()); if (batch == null || batch.getStatus() == null || batch.getStatus() != 1) { continue; } PromotionCouponRule rule = couponRuleMapper.selectRuleByBatchId(uc.getBatchId()); if (rule == null) { continue; } Map couponInfo = new LinkedHashMap<>(); couponInfo.put("id", uc.getId()); couponInfo.put("name", batch.getName()); couponInfo.put("couponType", batch.getCouponType()); couponInfo.put("isMutex", rule.getIsMutex()); couponInfo.put("threshold", rule.getThreshold()); couponInfo.put("amount", rule.getAmount()); couponInfo.put("discountRate", rule.getDiscountRate()); couponInfo.put("expireTime", uc.getExpireTime()); boolean usable = true; int isMutex = rule.getIsMutex() != null ? rule.getIsMutex() : 0; if (isMutex == 1 && promotionReduce.compareTo(BigDecimal.ZERO) > 0) { couponInfo.put("conflictWithPromotion", true); } if (rule.getThreshold() != null) { BigDecimal baseForCheck = isMutex == 1 ? originalAmount : afterPromotion; if (baseForCheck.compareTo(rule.getThreshold()) < 0) { usable = false; } } if (batch.getCouponType() != null && batch.getCouponType() == 2 && rule.getProductId() != null) { // 预览: 该商品券作用于的商品能优惠多少(名称/原价/优惠后价) CouponCalcResult preview = calcCouponReduce(rule, batch, itemsWithPrice, afterPromotion, priceMap, nameMap); if (preview.reduce.compareTo(BigDecimal.ZERO) > 0) { couponInfo.put("couponPreviewReduce", preview.reduce); } if (!preview.items.isEmpty()) { couponInfo.put("productPreview", preview.items); } boolean inCart = itemsWithPrice.stream() .anyMatch(i -> rule.getProductId().equals(toLong(i.get("productId")))); if (!inCart) { usable = false; } } couponInfo.put("usable", usable); available.add(couponInfo); } return available; } private Long toLong(Object obj) { if (obj == null) { return null; } if (obj instanceof Long) { return (Long) obj; } if (obj instanceof Number) { return ((Number) obj).longValue(); } try { return Long.parseLong(obj.toString()); } catch (NumberFormatException e) { return null; } } private int toInt(Object obj) { if (obj == null) { return 0; } if (obj instanceof Number) { return ((Number) obj).intValue(); } try { return Integer.parseInt(obj.toString()); } catch (NumberFormatException e) { return 0; } } }