|
|
@@ -0,0 +1,795 @@
|
|
|
+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.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(满减),取最优路径。
|
|
|
+ * 叠加新客立减、优惠券后得出最终金额。
|
|
|
+ *
|
|
|
+ * @author ruoyi
|
|
|
+ */
|
|
|
+@Service
|
|
|
+public class PromotionCalcServiceImpl implements IPromotionCalcService
|
|
|
+{
|
|
|
+ private static final BigDecimal MIN_AMOUNT = new BigDecimal("0.01");
|
|
|
+
|
|
|
+ @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 Map<String, Object> calculate(Long storeId, List<Map<String, Object>> items,
|
|
|
+ Long userId, Long couponId, String forcePath)
|
|
|
+ {
|
|
|
+ Map<String, Object> result = new LinkedHashMap<>();
|
|
|
+
|
|
|
+ // ---- 1. 获取商品真实价格,计算 originalAmount ----
|
|
|
+ Map<Long, BigDecimal> priceMap = new HashMap<>();
|
|
|
+ Map<Long, String> nameMap = new HashMap<>();
|
|
|
+ for (Map<String, Object> 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;
|
|
|
+ // itemsWithPrice: [{productId, quantity, unitPrice, name}]
|
|
|
+ List<Map<String, Object>> itemsWithPrice = new ArrayList<>();
|
|
|
+ for (Map<String, Object> 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<String, Object> 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(2, RoundingMode.HALF_UP);
|
|
|
+ result.put("originalAmount", originalAmount);
|
|
|
+
|
|
|
+ // ---- 2. 查询门店生效的促销活动 ----
|
|
|
+ List<PromotionActivity> activeActivities = activityMapper.selectActiveByStoreId(storeId);
|
|
|
+ List<PromotionActivityRule> activeRules = activityRuleMapper.selectActiveRulesByStoreId(storeId);
|
|
|
+
|
|
|
+ // 按活动类型分组
|
|
|
+ Map<Long, PromotionActivity> activityMap = activeActivities.stream()
|
|
|
+ .collect(Collectors.toMap(PromotionActivity::getId, a -> a, (a, b) -> a));
|
|
|
+
|
|
|
+ // activityId -> rules
|
|
|
+ Map<Long, List<PromotionActivityRule>> rulesByActivity = activeRules.stream()
|
|
|
+ .collect(Collectors.groupingBy(PromotionActivityRule::getActivityId));
|
|
|
+
|
|
|
+ // productId -> rules (for discount/half-price matching)
|
|
|
+ Map<Long, List<PromotionActivityRule>> rulesByProduct = activeRules.stream()
|
|
|
+ .filter(r -> r.getProductId() != null)
|
|
|
+ .collect(Collectors.groupingBy(PromotionActivityRule::getProductId));
|
|
|
+
|
|
|
+ // ---- 3. PATH A: 折扣(type=2) / 第二份半价(type=3) ----
|
|
|
+ Map<String, Object> pathAResult = calculatePathA(itemsWithPrice, activeActivities,
|
|
|
+ rulesByActivity, rulesByProduct, activityMap);
|
|
|
+ result.put("pathA", pathAResult);
|
|
|
+
|
|
|
+ // ---- 4. PATH B: 满减(type=1) ----
|
|
|
+ Map<String, Object> pathBResult = calculatePathB(itemsWithPrice, originalAmount,
|
|
|
+ activeActivities, rulesByActivity, activityMap);
|
|
|
+ result.put("pathB", pathBResult);
|
|
|
+
|
|
|
+ // ---- 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";
|
|
|
+ }
|
|
|
+ result.put("optimalPath", optimalPath);
|
|
|
+
|
|
|
+ BigDecimal afterPromotion = "A".equals(optimalPath) ? subtotalA : subtotalB;
|
|
|
+ BigDecimal promotionReduce = originalAmount.subtract(afterPromotion);
|
|
|
+
|
|
|
+ // ---- 6. 新客立减(type=4) ----
|
|
|
+ BigDecimal newCustomerReduce = BigDecimal.ZERO;
|
|
|
+ if (userId != null)
|
|
|
+ {
|
|
|
+ LambdaQueryWrapper<PosOrder> orderWrapper = new LambdaQueryWrapper<>();
|
|
|
+ orderWrapper.eq(PosOrder::getUserId, userId)
|
|
|
+ .eq(PosOrder::getMdId, storeId)
|
|
|
+ .eq(PosOrder::getState, 3L); // state=3 表示已完成
|
|
|
+ Long completedCount = posOrderMapper.selectCount(orderWrapper);
|
|
|
+
|
|
|
+ if (completedCount == 0)
|
|
|
+ {
|
|
|
+ // 新客
|
|
|
+ for (PromotionActivity act : activeActivities)
|
|
|
+ {
|
|
|
+ if (act.getType() != null && act.getType() == 4)
|
|
|
+ {
|
|
|
+ List<PromotionActivityRule> 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;
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+ if (newCustomerReduce.compareTo(BigDecimal.ZERO) > 0)
|
|
|
+ {
|
|
|
+ result.put("newCustomerReduce", newCustomerReduce);
|
|
|
+ }
|
|
|
+
|
|
|
+ // ---- 7. 优惠券 ----
|
|
|
+ BigDecimal couponReduce = BigDecimal.ZERO;
|
|
|
+ String couponName = null;
|
|
|
+ Boolean couponConflict = null;
|
|
|
+ String conflictNote = 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
|
|
|
+ {
|
|
|
+ PromotionCouponBatch 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 = "互斥券不可与满减/折扣叠加,选择此券将取消促销优惠";
|
|
|
+ // 互斥券:以原价为基础计算优惠
|
|
|
+ couponReduce = calcCouponReduce(couponRule, batch, itemsWithPrice, originalAmount, priceMap);
|
|
|
+ // 使用互斥券时,促销优惠取消,以原价减去券优惠
|
|
|
+ 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)
|
|
|
+ {
|
|
|
+ // 商品券(type=2)特殊检查:商品是否在折扣/半价活动中
|
|
|
+ if (batch.getCouponType() != null && batch.getCouponType() == 2
|
|
|
+ && couponRule.getProductId() != null && "A".equals(optimalPath))
|
|
|
+ {
|
|
|
+ // 检查商品券的商品是否在当前Path A的活动中
|
|
|
+ List<PromotionActivityRule> 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)
|
|
|
+ {
|
|
|
+ couponReduce = calcCouponReduce(couponRule, batch, itemsWithPrice,
|
|
|
+ baseForCoupon, priceMap);
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ if (couponId != null)
|
|
|
+ {
|
|
|
+ result.put("couponId", couponId);
|
|
|
+ }
|
|
|
+ if (couponName != null)
|
|
|
+ {
|
|
|
+ result.put("couponName", couponName);
|
|
|
+ }
|
|
|
+ if (couponConflict != null)
|
|
|
+ {
|
|
|
+ result.put("couponConflict", couponConflict);
|
|
|
+ }
|
|
|
+ if (conflictNote != null)
|
|
|
+ {
|
|
|
+ result.put("conflictNote", conflictNote);
|
|
|
+ }
|
|
|
+ if (couponReduce.compareTo(BigDecimal.ZERO) > 0)
|
|
|
+ {
|
|
|
+ result.put("couponReduce", couponReduce);
|
|
|
+ }
|
|
|
+
|
|
|
+ // ---- 8. 最终金额 ----
|
|
|
+ BigDecimal finalAmount = afterPromotion.subtract(newCustomerReduce).subtract(couponReduce);
|
|
|
+ finalAmount = finalAmount.max(MIN_AMOUNT).setScale(2, RoundingMode.HALF_UP);
|
|
|
+ result.put("finalAmount", finalAmount);
|
|
|
+
|
|
|
+ // ---- 9. 明细列表 ----
|
|
|
+ List<Map<String, Object>> details = new ArrayList<>();
|
|
|
+ if (promotionReduce.compareTo(BigDecimal.ZERO) > 0)
|
|
|
+ {
|
|
|
+ Map<String, Object> detail = new LinkedHashMap<>();
|
|
|
+ detail.put("type", "promotion");
|
|
|
+ if ("A".equals(optimalPath))
|
|
|
+ {
|
|
|
+ detail.put("subType", pathAResult.get("appliedType"));
|
|
|
+ detail.put("name", pathAResult.get("label"));
|
|
|
+ }
|
|
|
+ else
|
|
|
+ {
|
|
|
+ detail.put("subType", 1);
|
|
|
+ detail.put("name", pathBResult.get("label"));
|
|
|
+ }
|
|
|
+ detail.put("reduce", promotionReduce);
|
|
|
+ details.add(detail);
|
|
|
+ }
|
|
|
+ if (newCustomerReduce.compareTo(BigDecimal.ZERO) > 0)
|
|
|
+ {
|
|
|
+ Map<String, Object> detail = new LinkedHashMap<>();
|
|
|
+ detail.put("type", "promotion");
|
|
|
+ detail.put("subType", 4);
|
|
|
+ detail.put("name", "新客立减");
|
|
|
+ detail.put("reduce", newCustomerReduce);
|
|
|
+ details.add(detail);
|
|
|
+ }
|
|
|
+ if (couponReduce.compareTo(BigDecimal.ZERO) > 0)
|
|
|
+ {
|
|
|
+ Map<String, Object> detail = new LinkedHashMap<>();
|
|
|
+ detail.put("type", "coupon");
|
|
|
+ detail.put("subType", 0);
|
|
|
+ detail.put("name", couponName);
|
|
|
+ detail.put("reduce", couponReduce);
|
|
|
+ details.add(detail);
|
|
|
+ }
|
|
|
+ result.put("details", details);
|
|
|
+
|
|
|
+ // ---- 10. 可用优惠券列表 ----
|
|
|
+ List<Map<String, Object>> availableCoupons = buildAvailableCoupons(storeId, userId,
|
|
|
+ originalAmount, afterPromotion, promotionReduce, itemsWithPrice,
|
|
|
+ rulesByProduct, activityMap);
|
|
|
+ result.put("availableCoupons", availableCoupons);
|
|
|
+
|
|
|
+ return result;
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * PATH A: 折扣(type=2) vs 第二份半价(type=3),取更优
|
|
|
+ */
|
|
|
+ private Map<String, Object> calculatePathA(List<Map<String, Object>> itemsWithPrice,
|
|
|
+ List<PromotionActivity> activeActivities,
|
|
|
+ Map<Long, List<PromotionActivityRule>> rulesByActivity,
|
|
|
+ Map<Long, List<PromotionActivityRule>> rulesByProduct,
|
|
|
+ Map<Long, PromotionActivity> activityMap)
|
|
|
+ {
|
|
|
+ Map<String, Object> pathResult = new LinkedHashMap<>();
|
|
|
+ pathResult.put("label", "折扣");
|
|
|
+ pathResult.put("appliedType", 2);
|
|
|
+
|
|
|
+ // 分别计算折扣和第二份半价
|
|
|
+ Map<String, Object> discountResult = calcDiscount(itemsWithPrice, rulesByProduct, activityMap);
|
|
|
+ Map<String, Object> 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(2, 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(2, RoundingMode.HALF_UP) : BigDecimal.ZERO);
|
|
|
+ }
|
|
|
+
|
|
|
+ return pathResult;
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 折扣计算: 匹配到商品的规则 → unitPrice * discountRate
|
|
|
+ */
|
|
|
+ private Map<String, Object> calcDiscount(List<Map<String, Object>> itemsWithPrice,
|
|
|
+ Map<Long, List<PromotionActivityRule>> rulesByProduct,
|
|
|
+ Map<Long, PromotionActivity> activityMap)
|
|
|
+ {
|
|
|
+ Map<String, Object> result = new LinkedHashMap<>();
|
|
|
+ List<Map<String, Object>> itemList = new ArrayList<>();
|
|
|
+ BigDecimal subtotal = BigDecimal.ZERO;
|
|
|
+ BigDecimal originalRef = BigDecimal.ZERO;
|
|
|
+
|
|
|
+ for (Map<String, Object> 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<PromotionActivityRule> 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(2, RoundingMode.HALF_UP);
|
|
|
+ if (discounted.compareTo(finalPrice) < 0)
|
|
|
+ {
|
|
|
+ finalPrice = discounted;
|
|
|
+ matchedRule = rule;
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ Map<String, Object> lineItem = new LinkedHashMap<>();
|
|
|
+ lineItem.put("productId", productId);
|
|
|
+ lineItem.put("quantity", quantity);
|
|
|
+ lineItem.put("unitPrice", unitPrice);
|
|
|
+ lineItem.put("originalLineTotal", lineOriginal.setScale(2, RoundingMode.HALF_UP));
|
|
|
+ lineItem.put("finalLineTotal", finalPrice.setScale(2, 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(2, RoundingMode.HALF_UP));
|
|
|
+ result.put("originalRef", originalRef.setScale(2, RoundingMode.HALF_UP));
|
|
|
+ return result;
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 第二份半价计算: 每两件中的第二件按50%
|
|
|
+ */
|
|
|
+ private Map<String, Object> calcHalfPrice(List<Map<String, Object>> itemsWithPrice,
|
|
|
+ Map<Long, List<PromotionActivityRule>> rulesByProduct,
|
|
|
+ Map<Long, PromotionActivity> activityMap)
|
|
|
+ {
|
|
|
+ Map<String, Object> result = new LinkedHashMap<>();
|
|
|
+ List<Map<String, Object>> itemList = new ArrayList<>();
|
|
|
+ BigDecimal subtotal = BigDecimal.ZERO;
|
|
|
+ BigDecimal originalRef = BigDecimal.ZERO;
|
|
|
+
|
|
|
+ for (Map<String, Object> 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<PromotionActivityRule> 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())
|
|
|
+ {
|
|
|
+ // 每2件中第2件半价: pairs * 1.5 * unitPrice + remainder * unitPrice
|
|
|
+ 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(2, RoundingMode.HALF_UP);
|
|
|
+ if (halfPriceTotal.compareTo(finalPrice) < 0)
|
|
|
+ {
|
|
|
+ finalPrice = halfPriceTotal;
|
|
|
+ matchedRule = rule;
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ Map<String, Object> lineItem = new LinkedHashMap<>();
|
|
|
+ lineItem.put("productId", productId);
|
|
|
+ lineItem.put("quantity", quantity);
|
|
|
+ lineItem.put("unitPrice", unitPrice);
|
|
|
+ lineItem.put("originalLineTotal", lineOriginal.setScale(2, RoundingMode.HALF_UP));
|
|
|
+ lineItem.put("finalLineTotal", finalPrice.setScale(2, 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(2, RoundingMode.HALF_UP));
|
|
|
+ result.put("originalRef", originalRef.setScale(2, RoundingMode.HALF_UP));
|
|
|
+ return result;
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * PATH B: 满减(type=1) — 找最高匹配门槛的规则
|
|
|
+ */
|
|
|
+ private Map<String, Object> calculatePathB(List<Map<String, Object>> itemsWithPrice,
|
|
|
+ BigDecimal originalAmount,
|
|
|
+ List<PromotionActivity> activeActivities,
|
|
|
+ Map<Long, List<PromotionActivityRule>> rulesByActivity,
|
|
|
+ Map<Long, PromotionActivity> activityMap)
|
|
|
+ {
|
|
|
+ Map<String, Object> pathResult = new LinkedHashMap<>();
|
|
|
+ pathResult.put("label", "满减");
|
|
|
+
|
|
|
+ List<Map<String, Object>> itemList = new ArrayList<>();
|
|
|
+ for (Map<String, Object> item : itemsWithPrice)
|
|
|
+ {
|
|
|
+ Map<String, Object> 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(2, 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<PromotionActivityRule> 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(2, RoundingMode.HALF_UP);
|
|
|
+ pathResult.put("subtotal", subtotal);
|
|
|
+ pathResult.put("promotionReduce", bestMatch.getReduceAmount());
|
|
|
+ Map<String, Object> 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;
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 计算单张券的减免金额
|
|
|
+ */
|
|
|
+ private BigDecimal calcCouponReduce(PromotionCouponRule couponRule, PromotionCouponBatch batch,
|
|
|
+ List<Map<String, Object>> itemsWithPrice,
|
|
|
+ BigDecimal baseAmount, Map<Long, BigDecimal> priceMap)
|
|
|
+ {
|
|
|
+ if (batch.getCouponType() != null && batch.getCouponType() == 2)
|
|
|
+ {
|
|
|
+ // 商品券: 基于商品原价和折扣率
|
|
|
+ if (couponRule.getProductId() != null)
|
|
|
+ {
|
|
|
+ BigDecimal productPrice = priceMap.getOrDefault(couponRule.getProductId(), BigDecimal.ZERO);
|
|
|
+ // 找到此商品在购物车中的数量
|
|
|
+ int quantity = 0;
|
|
|
+ for (Map<String, Object> item : itemsWithPrice)
|
|
|
+ {
|
|
|
+ if (couponRule.getProductId().equals(toLong(item.get("productId"))))
|
|
|
+ {
|
|
|
+ quantity = toInt(item.get("quantity"));
|
|
|
+ break;
|
|
|
+ }
|
|
|
+ }
|
|
|
+ BigDecimal totalProductPrice = productPrice.multiply(BigDecimal.valueOf(quantity));
|
|
|
+ if (couponRule.getDiscountRate() != null)
|
|
|
+ {
|
|
|
+ BigDecimal reduction = totalProductPrice.multiply(BigDecimal.ONE.subtract(couponRule.getDiscountRate()));
|
|
|
+ return reduction.max(BigDecimal.ZERO).min(totalProductPrice)
|
|
|
+ .setScale(2, RoundingMode.HALF_UP);
|
|
|
+ }
|
|
|
+ else if (couponRule.getAmount() != null)
|
|
|
+ {
|
|
|
+ return couponRule.getAmount().min(totalProductPrice)
|
|
|
+ .setScale(2, RoundingMode.HALF_UP);
|
|
|
+ }
|
|
|
+ }
|
|
|
+ return BigDecimal.ZERO;
|
|
|
+ }
|
|
|
+ else
|
|
|
+ {
|
|
|
+ // 满减券: 直接减 amount
|
|
|
+ if (couponRule.getAmount() != null)
|
|
|
+ {
|
|
|
+ return couponRule.getAmount().min(baseAmount).setScale(2, RoundingMode.HALF_UP);
|
|
|
+ }
|
|
|
+ }
|
|
|
+ return BigDecimal.ZERO;
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 构建用户可用优惠券列表
|
|
|
+ */
|
|
|
+ private List<Map<String, Object>> buildAvailableCoupons(Long storeId, Long userId,
|
|
|
+ BigDecimal originalAmount,
|
|
|
+ BigDecimal afterPromotion,
|
|
|
+ BigDecimal promotionReduce,
|
|
|
+ List<Map<String, Object>> itemsWithPrice,
|
|
|
+ Map<Long, List<PromotionActivityRule>> rulesByProduct,
|
|
|
+ Map<Long, PromotionActivity> activityMap)
|
|
|
+ {
|
|
|
+ List<Map<String, Object>> available = new ArrayList<>();
|
|
|
+ if (userId == null)
|
|
|
+ {
|
|
|
+ return available;
|
|
|
+ }
|
|
|
+
|
|
|
+ // 查询用户未使用、未过期的优惠券
|
|
|
+ LambdaQueryWrapper<PromotionUserCoupon> wrapper = new LambdaQueryWrapper<>();
|
|
|
+ wrapper.eq(PromotionUserCoupon::getUserId, userId)
|
|
|
+ .eq(PromotionUserCoupon::getStoreId, storeId)
|
|
|
+ .eq(PromotionUserCoupon::getStatus, 0)
|
|
|
+ .gt(PromotionUserCoupon::getExpireTime, new Date());
|
|
|
+
|
|
|
+ List<PromotionUserCoupon> 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<String, Object> 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());
|
|
|
+
|
|
|
+ // 判断是否可用
|
|
|
+ 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)
|
|
|
+ {
|
|
|
+ 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;
|
|
|
+ }
|
|
|
+ }
|
|
|
+}
|