| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963 |
- 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<Map<String, Object>> items = new ArrayList<>();
- if (request.getItems() != null)
- {
- for (PromotionCalcRequest.CartItem ci : request.getItems())
- {
- Map<String, Object> m = new LinkedHashMap<>();
- m.put("productId", ci.getProductId());
- m.put("quantity", ci.getQuantity());
- items.add(m);
- }
- }
- // ---- 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;
- 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(0, RoundingMode.HALF_UP);
- // ---- 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));
- Map<Long, List<PromotionActivityRule>> rulesByActivity = activeRules.stream()
- .collect(Collectors.groupingBy(PromotionActivityRule::getActivityId));
- 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);
- // ---- 4. PATH B: 满减(type=1) ----
- Map<String, Object> 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<PosOrder> 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<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;
- }
- }
- }
- }
- // ---- 7. 优惠券 ----
- BigDecimal couponReduce = BigDecimal.ZERO;
- String couponName = null;
- Boolean couponConflict = null;
- String conflictNote = null;
- PromotionCouponBatch batch = null;
- // 商品券生效时, 记录参与优惠的商品明细(名称/原价/优惠后价)
- List<Map<String, Object>> 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<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)
- {
- 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<PromotionDetail> 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<String, Object> matchedRule = (Map<String, Object>) 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<Map<String, Object>> 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<String, Object> 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<Map<String, Object>> itemList = (List<Map<String, Object>>) pathMap.get("items");
- if (itemList != null)
- {
- pr.setItems(itemList.stream().map(this::toLineItem).collect(Collectors.toList()));
- }
- @SuppressWarnings("unchecked")
- Map<String, Object> matchedRuleMap = (Map<String, Object>) 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<String, Object> 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<String, Object> 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<Map<String, Object>> productPreview = (List<Map<String, Object>>) 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<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(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<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(0, 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(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<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())
- {
- 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<String, Object> 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<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(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<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(0, RoundingMode.HALF_UP);
- pathResult.put("subtotal", subtotal);
- pathResult.put("promotionReduce", originalAmount.subtract(subtotal));
- 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;
- }
- /**
- * 券减免计算结果: reduce=减免金额, items=商品券参与的商品明细(仅商品券有)
- */
- private static class CouponCalcResult
- {
- BigDecimal reduce = BigDecimal.ZERO;
- List<Map<String, Object>> items = new ArrayList<>();
- }
- /**
- * 计算单张券的减免金额; 商品券(couponType=2)同时返回参与商品明细(名称/原价/优惠后价)
- */
- private CouponCalcResult calcCouponReduce(PromotionCouponRule couponRule, PromotionCouponBatch batch,
- List<Map<String, Object>> itemsWithPrice,
- BigDecimal baseAmount, Map<Long, BigDecimal> priceMap,
- Map<Long, String> 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<String, Object> 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<String, Object> 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<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,
- Map<Long, BigDecimal> priceMap,
- Map<Long, String> nameMap)
- {
- 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());
- 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;
- }
- }
- }
|