PromotionCalcServiceImpl.java 41 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963
  1. package com.ruoyi.system.service.impl;
  2. import java.math.BigDecimal;
  3. import java.math.RoundingMode;
  4. import java.util.ArrayList;
  5. import java.util.Date;
  6. import java.util.HashMap;
  7. import java.util.LinkedHashMap;
  8. import java.util.List;
  9. import java.util.Map;
  10. import java.util.stream.Collectors;
  11. import org.springframework.beans.factory.annotation.Autowired;
  12. import org.springframework.stereotype.Service;
  13. import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
  14. import com.ruoyi.system.domain.PosFood;
  15. import com.ruoyi.system.domain.PosOrder;
  16. import com.ruoyi.system.domain.PromotionActivity;
  17. import com.ruoyi.system.domain.PromotionActivityRule;
  18. import com.ruoyi.system.domain.PromotionCouponBatch;
  19. import com.ruoyi.system.domain.PromotionCouponRule;
  20. import com.ruoyi.system.domain.PromotionUserCoupon;
  21. import com.ruoyi.system.dto.PromotionCalcRequest;
  22. import com.ruoyi.system.dto.PromotionCalcResponse;
  23. import com.ruoyi.system.dto.PromotionCalcResponse.AvailableCoupon;
  24. import com.ruoyi.system.dto.PromotionCalcResponse.LineItem;
  25. import com.ruoyi.system.dto.PromotionCalcResponse.MatchedRule;
  26. import com.ruoyi.system.dto.PromotionCalcResponse.PathResult;
  27. import com.ruoyi.system.dto.PromotionCalcResponse.PromotionDetail;
  28. import com.ruoyi.system.mapper.PosFoodMapper;
  29. import com.ruoyi.system.mapper.PosOrderMapper;
  30. import com.ruoyi.system.mapper.PromotionActivityMapper;
  31. import com.ruoyi.system.mapper.PromotionActivityRuleMapper;
  32. import com.ruoyi.system.mapper.PromotionCouponBatchMapper;
  33. import com.ruoyi.system.mapper.PromotionCouponRuleMapper;
  34. import com.ruoyi.system.mapper.PromotionUserCouponMapper;
  35. import com.ruoyi.system.service.IPromotionCalcService;
  36. /**
  37. * 促销算价Service实现
  38. *
  39. * 两路算价:Path A(折扣/第二份半价)vs Path B(满减),取最优路径。
  40. * 叠加新客立减、优惠券后得出最终金额。
  41. */
  42. @Service
  43. public class PromotionCalcServiceImpl implements IPromotionCalcService
  44. {
  45. private static final BigDecimal MIN_AMOUNT = new BigDecimal("1");
  46. @Autowired
  47. private PromotionActivityMapper activityMapper;
  48. @Autowired
  49. private PromotionActivityRuleMapper activityRuleMapper;
  50. @Autowired
  51. private PromotionCouponBatchMapper couponBatchMapper;
  52. @Autowired
  53. private PromotionCouponRuleMapper couponRuleMapper;
  54. @Autowired
  55. private PromotionUserCouponMapper userCouponMapper;
  56. @Autowired
  57. private PosFoodMapper posFoodMapper;
  58. @Autowired
  59. private PosOrderMapper posOrderMapper;
  60. @Override
  61. public PromotionCalcResponse calculate(PromotionCalcRequest request, Long userId)
  62. {
  63. Long storeId = request.getStoreId();
  64. Long couponId = request.getCouponId();
  65. String forcePath = request.getForcePath();
  66. // 将 DTO items 转为内部 Map 格式
  67. List<Map<String, Object>> items = new ArrayList<>();
  68. if (request.getItems() != null)
  69. {
  70. for (PromotionCalcRequest.CartItem ci : request.getItems())
  71. {
  72. Map<String, Object> m = new LinkedHashMap<>();
  73. m.put("productId", ci.getProductId());
  74. m.put("quantity", ci.getQuantity());
  75. items.add(m);
  76. }
  77. }
  78. // ---- 1. 获取商品真实价格,计算 originalAmount ----
  79. Map<Long, BigDecimal> priceMap = new HashMap<>();
  80. Map<Long, String> nameMap = new HashMap<>();
  81. for (Map<String, Object> item : items)
  82. {
  83. Long productId = toLong(item.get("productId"));
  84. if (productId != null && !priceMap.containsKey(productId))
  85. {
  86. PosFood food = posFoodMapper.selectById(productId);
  87. if (food != null)
  88. {
  89. priceMap.put(productId, food.getPrice());
  90. nameMap.put(productId, food.getName());
  91. }
  92. }
  93. }
  94. BigDecimal originalAmount = BigDecimal.ZERO;
  95. List<Map<String, Object>> itemsWithPrice = new ArrayList<>();
  96. for (Map<String, Object> item : items)
  97. {
  98. Long productId = toLong(item.get("productId"));
  99. int quantity = toInt(item.get("quantity"));
  100. BigDecimal unitPrice = priceMap.getOrDefault(productId, BigDecimal.ZERO);
  101. originalAmount = originalAmount.add(unitPrice.multiply(BigDecimal.valueOf(quantity)));
  102. Map<String, Object> ip = new LinkedHashMap<>();
  103. ip.put("productId", productId);
  104. ip.put("quantity", quantity);
  105. ip.put("unitPrice", unitPrice);
  106. ip.put("name", nameMap.getOrDefault(productId, ""));
  107. itemsWithPrice.add(ip);
  108. }
  109. originalAmount = originalAmount.setScale(0, RoundingMode.HALF_UP);
  110. // ---- 2. 查询门店生效的促销活动 ----
  111. List<PromotionActivity> activeActivities = activityMapper.selectActiveByStoreId(storeId);
  112. List<PromotionActivityRule> activeRules = activityRuleMapper.selectActiveRulesByStoreId(storeId);
  113. Map<Long, PromotionActivity> activityMap = activeActivities.stream()
  114. .collect(Collectors.toMap(PromotionActivity::getId, a -> a, (a, b) -> a));
  115. Map<Long, List<PromotionActivityRule>> rulesByActivity = activeRules.stream()
  116. .collect(Collectors.groupingBy(PromotionActivityRule::getActivityId));
  117. Map<Long, List<PromotionActivityRule>> rulesByProduct = activeRules.stream()
  118. .filter(r -> r.getProductId() != null)
  119. .collect(Collectors.groupingBy(PromotionActivityRule::getProductId));
  120. // ---- 3. PATH A: 折扣(type=2) / 第二份半价(type=3) ----
  121. Map<String, Object> pathAResult = calculatePathA(itemsWithPrice, activeActivities,
  122. rulesByActivity, rulesByProduct, activityMap);
  123. // ---- 4. PATH B: 满减(type=1) ----
  124. Map<String, Object> pathBResult = calculatePathB(itemsWithPrice, originalAmount,
  125. activeActivities, rulesByActivity, activityMap);
  126. // ---- 5. 比较两条路径 ----
  127. BigDecimal subtotalA = (BigDecimal) pathAResult.get("subtotal");
  128. BigDecimal subtotalB = (BigDecimal) pathBResult.get("subtotal");
  129. String optimalPath;
  130. if (forcePath != null && ("A".equalsIgnoreCase(forcePath) || "B".equalsIgnoreCase(forcePath)))
  131. {
  132. optimalPath = forcePath.toUpperCase();
  133. }
  134. else
  135. {
  136. optimalPath = subtotalA.compareTo(subtotalB) < 0 ? "A" : "B";
  137. }
  138. BigDecimal afterPromotion = "A".equals(optimalPath) ? subtotalA : subtotalB;
  139. BigDecimal promotionReduce = originalAmount.subtract(afterPromotion);
  140. // ---- 6. 新客立减(type=4) ----
  141. BigDecimal newCustomerReduce = BigDecimal.ZERO;
  142. if (userId != null)
  143. {
  144. LambdaQueryWrapper<PosOrder> orderWrapper = new LambdaQueryWrapper<>();
  145. orderWrapper.eq(PosOrder::getUserId, userId)
  146. .eq(PosOrder::getMdId, storeId)
  147. .eq(PosOrder::getPayStatus, 1L);
  148. Long paidCount = posOrderMapper.selectCount(orderWrapper);
  149. if (paidCount == 0)
  150. {
  151. for (PromotionActivity act : activeActivities)
  152. {
  153. if (act.getType() != null && act.getType() == 4)
  154. {
  155. List<PromotionActivityRule> ncRules = rulesByActivity.get(act.getId());
  156. if (ncRules != null && !ncRules.isEmpty())
  157. {
  158. BigDecimal reduce = ncRules.get(0).getReduceAmount();
  159. if (reduce != null && reduce.compareTo(BigDecimal.ZERO) > 0)
  160. {
  161. newCustomerReduce = reduce;
  162. }
  163. }
  164. break;
  165. }
  166. }
  167. }
  168. }
  169. // ---- 7. 优惠券 ----
  170. BigDecimal couponReduce = BigDecimal.ZERO;
  171. String couponName = null;
  172. Boolean couponConflict = null;
  173. String conflictNote = null;
  174. PromotionCouponBatch batch = null;
  175. // 商品券生效时, 记录参与优惠的商品明细(名称/原价/优惠后价)
  176. List<Map<String, Object>> couponProductItems = null;
  177. if (couponId != null)
  178. {
  179. PromotionUserCoupon userCoupon = userCouponMapper.selectById(couponId);
  180. if (userCoupon != null && userCoupon.getUserId().equals(userId)
  181. && userCoupon.getStatus() != null && userCoupon.getStatus() == 0)
  182. {
  183. if (userCoupon.getExpireTime() != null && userCoupon.getExpireTime().before(new Date()))
  184. {
  185. // 已过期,不处理
  186. }
  187. else
  188. {
  189. batch = couponBatchMapper.selectById(userCoupon.getBatchId());
  190. PromotionCouponRule couponRule = couponRuleMapper.selectRuleByBatchId(userCoupon.getBatchId());
  191. if (batch != null && couponRule != null)
  192. {
  193. couponName = batch.getName();
  194. int isMutex = couponRule.getIsMutex() != null ? couponRule.getIsMutex() : 0;
  195. if (isMutex == 1 && promotionReduce.compareTo(BigDecimal.ZERO) > 0)
  196. {
  197. couponConflict = true;
  198. conflictNote = "互斥券不可与满减/折扣叠加,选择此券将取消促销优惠";
  199. CouponCalcResult mutexResult = calcCouponReduce(couponRule, batch, itemsWithPrice, originalAmount, priceMap, nameMap);
  200. couponReduce = mutexResult.reduce;
  201. couponProductItems = mutexResult.items;
  202. afterPromotion = originalAmount;
  203. promotionReduce = BigDecimal.ZERO;
  204. newCustomerReduce = BigDecimal.ZERO;
  205. }
  206. else
  207. {
  208. BigDecimal baseForCoupon = afterPromotion.subtract(newCustomerReduce);
  209. baseForCoupon = baseForCoupon.max(BigDecimal.ZERO);
  210. if (couponRule.getThreshold() == null
  211. || baseForCoupon.compareTo(couponRule.getThreshold()) >= 0)
  212. {
  213. if (batch.getCouponType() != null && batch.getCouponType() == 2
  214. && couponRule.getProductId() != null && "A".equals(optimalPath))
  215. {
  216. List<PromotionActivityRule> productRules = rulesByProduct.get(couponRule.getProductId());
  217. if (productRules != null)
  218. {
  219. boolean inDiscount = productRules.stream().anyMatch(r ->
  220. {
  221. PromotionActivity act = activityMap.get(r.getActivityId());
  222. return act != null && (act.getType() == 2 || act.getType() == 3);
  223. });
  224. if (inDiscount)
  225. {
  226. couponConflict = true;
  227. conflictNote = "商品券对应的商品在折扣活动中,不可与Path A叠加";
  228. }
  229. }
  230. }
  231. if (couponConflict == null)
  232. {
  233. CouponCalcResult normalResult = calcCouponReduce(couponRule, batch, itemsWithPrice,
  234. baseForCoupon, priceMap, nameMap);
  235. couponReduce = normalResult.reduce;
  236. couponProductItems = normalResult.items;
  237. }
  238. }
  239. }
  240. }
  241. }
  242. }
  243. }
  244. // ---- 8. 最终金额 ----
  245. BigDecimal finalAmount = afterPromotion.subtract(newCustomerReduce).subtract(couponReduce);
  246. finalAmount = finalAmount.max(MIN_AMOUNT).setScale(0, RoundingMode.HALF_UP);
  247. // ---- 9. 构建响应DTO ----
  248. PromotionCalcResponse response = new PromotionCalcResponse();
  249. response.setOriginalAmount(originalAmount);
  250. response.setPathA(toPathResult(pathAResult));
  251. response.setPathB(toPathResult(pathBResult));
  252. response.setOptimalPath(optimalPath);
  253. response.setFinalAmount(finalAmount);
  254. if (newCustomerReduce.compareTo(BigDecimal.ZERO) > 0)
  255. {
  256. response.setNewCustomerReduce(newCustomerReduce);
  257. }
  258. if (couponId != null)
  259. {
  260. response.setCouponId(couponId);
  261. }
  262. if (couponName != null)
  263. {
  264. response.setCouponName(couponName);
  265. }
  266. if (couponReduce.compareTo(BigDecimal.ZERO) > 0)
  267. {
  268. response.setCouponReduce(couponReduce);
  269. }
  270. if (couponConflict != null)
  271. {
  272. response.setCouponConflict(couponConflict);
  273. }
  274. if (conflictNote != null)
  275. {
  276. response.setConflictNote(conflictNote);
  277. }
  278. // 设置优惠券批次ID(用于下单快照写入)
  279. if (couponId != null && batch != null)
  280. {
  281. response.setCouponBatchId(batch.getId());
  282. }
  283. // 商品券: 返回参与优惠的商品明细(名称/原价/优惠后价)
  284. if (couponProductItems != null && !couponProductItems.isEmpty())
  285. {
  286. response.setCouponProductItems(couponProductItems.stream()
  287. .map(this::toLineItem).collect(Collectors.toList()));
  288. }
  289. // ---- 10. 明细列表 ----
  290. List<PromotionDetail> details = new ArrayList<>();
  291. if (promotionReduce.compareTo(BigDecimal.ZERO) > 0)
  292. {
  293. PromotionDetail detail = new PromotionDetail();
  294. detail.setType("promotion");
  295. if ("A".equals(optimalPath))
  296. {
  297. detail.setSubType((Integer) pathAResult.get("appliedType"));
  298. detail.setName((String) pathAResult.get("label"));
  299. // 找到对应类型的促销活动ID
  300. Integer appliedType = (Integer) pathAResult.get("appliedType");
  301. for (PromotionActivity act : activeActivities)
  302. {
  303. if (act.getType() != null && act.getType().equals(appliedType))
  304. {
  305. detail.setRefId(act.getId());
  306. break;
  307. }
  308. }
  309. }
  310. else
  311. {
  312. detail.setSubType(1);
  313. detail.setName((String) pathBResult.get("label"));
  314. // 从满减匹配规则中获取活动ID
  315. @SuppressWarnings("unchecked")
  316. Map<String, Object> matchedRule = (Map<String, Object>) pathBResult.get("matchedRule");
  317. if (matchedRule != null)
  318. {
  319. detail.setRefId(toLong(matchedRule.get("activityId")));
  320. }
  321. }
  322. detail.setReduce(promotionReduce);
  323. details.add(detail);
  324. }
  325. if (newCustomerReduce.compareTo(BigDecimal.ZERO) > 0)
  326. {
  327. PromotionDetail detail = new PromotionDetail();
  328. detail.setType("promotion");
  329. detail.setSubType(4);
  330. detail.setName("新客立减");
  331. detail.setReduce(newCustomerReduce);
  332. // 找到新客立减活动ID
  333. for (PromotionActivity act : activeActivities)
  334. {
  335. if (act.getType() != null && act.getType() == 4)
  336. {
  337. detail.setRefId(act.getId());
  338. break;
  339. }
  340. }
  341. details.add(detail);
  342. }
  343. if (couponReduce.compareTo(BigDecimal.ZERO) > 0)
  344. {
  345. PromotionDetail detail = new PromotionDetail();
  346. detail.setType("coupon");
  347. detail.setSubType(0);
  348. detail.setName(couponName);
  349. detail.setReduce(couponReduce);
  350. // 优惠券的refId设为券批次ID
  351. if (batch != null)
  352. {
  353. detail.setRefId(batch.getId());
  354. }
  355. details.add(detail);
  356. }
  357. response.setDetails(details);
  358. // ---- 11. 可用优惠券列表 ----
  359. List<Map<String, Object>> availableCouponMaps = buildAvailableCoupons(storeId, userId,
  360. originalAmount, afterPromotion, promotionReduce, itemsWithPrice,
  361. rulesByProduct, activityMap, priceMap, nameMap);
  362. response.setAvailableCoupons(availableCouponMaps.stream()
  363. .map(this::toAvailableCoupon)
  364. .collect(Collectors.toList()));
  365. return response;
  366. }
  367. // ==================== DTO转换方法 ====================
  368. private PathResult toPathResult(Map<String, Object> pathMap)
  369. {
  370. PathResult pr = new PathResult();
  371. pr.setLabel((String) pathMap.get("label"));
  372. pr.setSubtotal((BigDecimal) pathMap.get("subtotal"));
  373. if (pathMap.containsKey("appliedType"))
  374. {
  375. pr.setAppliedType((Integer) pathMap.get("appliedType"));
  376. }
  377. if (pathMap.containsKey("promotionReduce"))
  378. {
  379. pr.setPromotionReduce((BigDecimal) pathMap.get("promotionReduce"));
  380. }
  381. @SuppressWarnings("unchecked")
  382. List<Map<String, Object>> itemList = (List<Map<String, Object>>) pathMap.get("items");
  383. if (itemList != null)
  384. {
  385. pr.setItems(itemList.stream().map(this::toLineItem).collect(Collectors.toList()));
  386. }
  387. @SuppressWarnings("unchecked")
  388. Map<String, Object> matchedRuleMap = (Map<String, Object>) pathMap.get("matchedRule");
  389. if (matchedRuleMap != null)
  390. {
  391. MatchedRule mr = new MatchedRule();
  392. mr.setId(toLong(matchedRuleMap.get("id")));
  393. mr.setActivityId(toLong(matchedRuleMap.get("activityId")));
  394. mr.setActivityName((String) matchedRuleMap.get("activityName"));
  395. mr.setThreshold((BigDecimal) matchedRuleMap.get("threshold"));
  396. mr.setReduceAmount((BigDecimal) matchedRuleMap.get("reduceAmount"));
  397. pr.setMatchedRule(mr);
  398. }
  399. return pr;
  400. }
  401. private LineItem toLineItem(Map<String, Object> itemMap)
  402. {
  403. LineItem li = new LineItem();
  404. li.setProductId(toLong(itemMap.get("productId")));
  405. li.setQuantity(toInt(itemMap.get("quantity")));
  406. li.setUnitPrice((BigDecimal) itemMap.get("unitPrice"));
  407. li.setName((String) itemMap.get("name"));
  408. li.setOriginalLineTotal((BigDecimal) itemMap.get("originalLineTotal"));
  409. li.setFinalLineTotal((BigDecimal) itemMap.get("finalLineTotal"));
  410. li.setDiscountRate((BigDecimal) itemMap.get("discountRate"));
  411. li.setHalfPriceApplied((Boolean) itemMap.get("halfPriceApplied"));
  412. return li;
  413. }
  414. private AvailableCoupon toAvailableCoupon(Map<String, Object> couponMap)
  415. {
  416. AvailableCoupon ac = new AvailableCoupon();
  417. ac.setId(toLong(couponMap.get("id")));
  418. ac.setName((String) couponMap.get("name"));
  419. ac.setCouponType((Integer) couponMap.get("couponType"));
  420. ac.setIsMutex((Integer) couponMap.get("isMutex"));
  421. ac.setThreshold((BigDecimal) couponMap.get("threshold"));
  422. ac.setAmount((BigDecimal) couponMap.get("amount"));
  423. ac.setDiscountRate((BigDecimal) couponMap.get("discountRate"));
  424. ac.setUsable((Boolean) couponMap.get("usable"));
  425. ac.setConflictWithPromotion((Boolean) couponMap.get("conflictWithPromotion"));
  426. ac.setCouponPreviewReduce((BigDecimal) couponMap.get("couponPreviewReduce"));
  427. @SuppressWarnings("unchecked")
  428. List<Map<String, Object>> productPreview = (List<Map<String, Object>>) couponMap.get("productPreview");
  429. if (productPreview != null && !productPreview.isEmpty())
  430. {
  431. ac.setProductPreview(productPreview.stream().map(this::toLineItem).collect(Collectors.toList()));
  432. }
  433. ac.setExpireTime((java.util.Date) couponMap.get("expireTime"));
  434. return ac;
  435. }
  436. // ==================== 内部计算方法(保持Map) ====================
  437. /**
  438. * PATH A: 折扣(type=2) vs 第二份半价(type=3),取更优
  439. */
  440. private Map<String, Object> calculatePathA(List<Map<String, Object>> itemsWithPrice,
  441. List<PromotionActivity> activeActivities,
  442. Map<Long, List<PromotionActivityRule>> rulesByActivity,
  443. Map<Long, List<PromotionActivityRule>> rulesByProduct,
  444. Map<Long, PromotionActivity> activityMap)
  445. {
  446. Map<String, Object> pathResult = new LinkedHashMap<>();
  447. pathResult.put("label", "折扣");
  448. pathResult.put("appliedType", 2);
  449. Map<String, Object> discountResult = calcDiscount(itemsWithPrice, rulesByProduct, activityMap);
  450. Map<String, Object> halfPriceResult = calcHalfPrice(itemsWithPrice, rulesByProduct, activityMap);
  451. BigDecimal discountSubtotal = (BigDecimal) discountResult.get("subtotal");
  452. BigDecimal halfPriceSubtotal = (BigDecimal) halfPriceResult.get("subtotal");
  453. if (halfPriceSubtotal.compareTo(discountSubtotal) < 0)
  454. {
  455. pathResult.put("label", "第二份半价");
  456. pathResult.put("appliedType", 3);
  457. pathResult.put("items", halfPriceResult.get("items"));
  458. pathResult.put("subtotal", halfPriceSubtotal);
  459. pathResult.put("promotionReduce",
  460. discountResult.containsKey("originalRef")
  461. ? ((BigDecimal) discountResult.get("originalRef")).subtract(halfPriceSubtotal)
  462. .setScale(0, RoundingMode.HALF_UP) : BigDecimal.ZERO);
  463. }
  464. else
  465. {
  466. pathResult.put("items", discountResult.get("items"));
  467. pathResult.put("subtotal", discountSubtotal);
  468. pathResult.put("promotionReduce",
  469. discountResult.containsKey("originalRef")
  470. ? ((BigDecimal) discountResult.get("originalRef")).subtract(discountSubtotal)
  471. .setScale(0, RoundingMode.HALF_UP) : BigDecimal.ZERO);
  472. }
  473. return pathResult;
  474. }
  475. /**
  476. * 折扣计算: 匹配到商品的规则 → unitPrice * discountRate
  477. */
  478. private Map<String, Object> calcDiscount(List<Map<String, Object>> itemsWithPrice,
  479. Map<Long, List<PromotionActivityRule>> rulesByProduct,
  480. Map<Long, PromotionActivity> activityMap)
  481. {
  482. Map<String, Object> result = new LinkedHashMap<>();
  483. List<Map<String, Object>> itemList = new ArrayList<>();
  484. BigDecimal subtotal = BigDecimal.ZERO;
  485. BigDecimal originalRef = BigDecimal.ZERO;
  486. for (Map<String, Object> item : itemsWithPrice)
  487. {
  488. Long productId = toLong(item.get("productId"));
  489. int quantity = toInt(item.get("quantity"));
  490. BigDecimal unitPrice = (BigDecimal) item.get("unitPrice");
  491. BigDecimal lineOriginal = unitPrice.multiply(BigDecimal.valueOf(quantity));
  492. originalRef = originalRef.add(lineOriginal);
  493. BigDecimal finalPrice = lineOriginal;
  494. PromotionActivityRule matchedRule = null;
  495. List<PromotionActivityRule> productRules = rulesByProduct.get(productId);
  496. if (productRules != null)
  497. {
  498. for (PromotionActivityRule rule : productRules)
  499. {
  500. PromotionActivity act = activityMap.get(rule.getActivityId());
  501. if (act != null && act.getType() == 2 && rule.getDiscountRate() != null)
  502. {
  503. if (rule.getMinQuantity() == null || quantity >= rule.getMinQuantity())
  504. {
  505. BigDecimal discounted = unitPrice.multiply(rule.getDiscountRate())
  506. .multiply(BigDecimal.valueOf(quantity))
  507. .setScale(0, RoundingMode.HALF_UP);
  508. if (discounted.compareTo(finalPrice) < 0)
  509. {
  510. finalPrice = discounted;
  511. matchedRule = rule;
  512. }
  513. }
  514. }
  515. }
  516. }
  517. Map<String, Object> lineItem = new LinkedHashMap<>();
  518. lineItem.put("productId", productId);
  519. lineItem.put("quantity", quantity);
  520. lineItem.put("unitPrice", unitPrice);
  521. lineItem.put("originalLineTotal", lineOriginal.setScale(0, RoundingMode.HALF_UP));
  522. lineItem.put("finalLineTotal", finalPrice.setScale(0, RoundingMode.HALF_UP));
  523. lineItem.put("name", item.get("name"));
  524. if (matchedRule != null)
  525. {
  526. lineItem.put("discountRate", matchedRule.getDiscountRate());
  527. }
  528. itemList.add(lineItem);
  529. subtotal = subtotal.add(finalPrice);
  530. }
  531. result.put("items", itemList);
  532. result.put("subtotal", subtotal.setScale(0, RoundingMode.HALF_UP));
  533. result.put("originalRef", originalRef.setScale(0, RoundingMode.HALF_UP));
  534. return result;
  535. }
  536. /**
  537. * 第二份半价计算: 每两件中的第二件按50%
  538. */
  539. private Map<String, Object> calcHalfPrice(List<Map<String, Object>> itemsWithPrice,
  540. Map<Long, List<PromotionActivityRule>> rulesByProduct,
  541. Map<Long, PromotionActivity> activityMap)
  542. {
  543. Map<String, Object> result = new LinkedHashMap<>();
  544. List<Map<String, Object>> itemList = new ArrayList<>();
  545. BigDecimal subtotal = BigDecimal.ZERO;
  546. BigDecimal originalRef = BigDecimal.ZERO;
  547. for (Map<String, Object> item : itemsWithPrice)
  548. {
  549. Long productId = toLong(item.get("productId"));
  550. int quantity = toInt(item.get("quantity"));
  551. BigDecimal unitPrice = (BigDecimal) item.get("unitPrice");
  552. BigDecimal lineOriginal = unitPrice.multiply(BigDecimal.valueOf(quantity));
  553. originalRef = originalRef.add(lineOriginal);
  554. BigDecimal finalPrice = lineOriginal;
  555. PromotionActivityRule matchedRule = null;
  556. List<PromotionActivityRule> productRules = rulesByProduct.get(productId);
  557. if (productRules != null)
  558. {
  559. for (PromotionActivityRule rule : productRules)
  560. {
  561. PromotionActivity act = activityMap.get(rule.getActivityId());
  562. if (act != null && act.getType() == 3)
  563. {
  564. if (rule.getMinQuantity() == null || quantity >= rule.getMinQuantity())
  565. {
  566. int pairs = quantity / 2;
  567. int remainder = quantity % 2;
  568. BigDecimal halfPriceTotal = unitPrice.multiply(BigDecimal.valueOf(pairs))
  569. .multiply(new BigDecimal("1.5"))
  570. .add(unitPrice.multiply(BigDecimal.valueOf(remainder)))
  571. .setScale(0, RoundingMode.HALF_UP);
  572. if (halfPriceTotal.compareTo(finalPrice) < 0)
  573. {
  574. finalPrice = halfPriceTotal;
  575. matchedRule = rule;
  576. }
  577. }
  578. }
  579. }
  580. }
  581. Map<String, Object> lineItem = new LinkedHashMap<>();
  582. lineItem.put("productId", productId);
  583. lineItem.put("quantity", quantity);
  584. lineItem.put("unitPrice", unitPrice);
  585. lineItem.put("originalLineTotal", lineOriginal.setScale(0, RoundingMode.HALF_UP));
  586. lineItem.put("finalLineTotal", finalPrice.setScale(0, RoundingMode.HALF_UP));
  587. lineItem.put("name", item.get("name"));
  588. if (matchedRule != null)
  589. {
  590. lineItem.put("halfPriceApplied", true);
  591. }
  592. itemList.add(lineItem);
  593. subtotal = subtotal.add(finalPrice);
  594. }
  595. result.put("items", itemList);
  596. result.put("subtotal", subtotal.setScale(0, RoundingMode.HALF_UP));
  597. result.put("originalRef", originalRef.setScale(0, RoundingMode.HALF_UP));
  598. return result;
  599. }
  600. /**
  601. * PATH B: 满减(type=1) — 找最高匹配门槛的规则
  602. */
  603. private Map<String, Object> calculatePathB(List<Map<String, Object>> itemsWithPrice,
  604. BigDecimal originalAmount,
  605. List<PromotionActivity> activeActivities,
  606. Map<Long, List<PromotionActivityRule>> rulesByActivity,
  607. Map<Long, PromotionActivity> activityMap)
  608. {
  609. Map<String, Object> pathResult = new LinkedHashMap<>();
  610. pathResult.put("label", "满减");
  611. List<Map<String, Object>> itemList = new ArrayList<>();
  612. for (Map<String, Object> item : itemsWithPrice)
  613. {
  614. Map<String, Object> lineItem = new LinkedHashMap<>();
  615. lineItem.put("productId", item.get("productId"));
  616. lineItem.put("quantity", item.get("quantity"));
  617. lineItem.put("unitPrice", item.get("unitPrice"));
  618. BigDecimal lineTotal = ((BigDecimal) item.get("unitPrice"))
  619. .multiply(BigDecimal.valueOf(toInt(item.get("quantity"))))
  620. .setScale(0, RoundingMode.HALF_UP);
  621. lineItem.put("originalLineTotal", lineTotal);
  622. lineItem.put("finalLineTotal", lineTotal);
  623. lineItem.put("name", item.get("name"));
  624. itemList.add(lineItem);
  625. }
  626. pathResult.put("items", itemList);
  627. PromotionActivityRule bestMatch = null;
  628. PromotionActivity bestActivity = null;
  629. for (PromotionActivity act : activeActivities)
  630. {
  631. if (act.getType() != null && act.getType() == 1)
  632. {
  633. List<PromotionActivityRule> rules = rulesByActivity.get(act.getId());
  634. if (rules != null)
  635. {
  636. for (PromotionActivityRule rule : rules)
  637. {
  638. if (rule.getThreshold() != null && rule.getReduceAmount() != null
  639. && originalAmount.compareTo(rule.getThreshold()) >= 0)
  640. {
  641. if (bestMatch == null || rule.getThreshold().compareTo(bestMatch.getThreshold()) > 0)
  642. {
  643. bestMatch = rule;
  644. bestActivity = act;
  645. }
  646. }
  647. }
  648. }
  649. }
  650. }
  651. if (bestMatch != null)
  652. {
  653. BigDecimal subtotal = originalAmount.subtract(bestMatch.getReduceAmount())
  654. .max(MIN_AMOUNT).setScale(0, RoundingMode.HALF_UP);
  655. pathResult.put("subtotal", subtotal);
  656. pathResult.put("promotionReduce", originalAmount.subtract(subtotal));
  657. Map<String, Object> matchedRuleInfo = new LinkedHashMap<>();
  658. matchedRuleInfo.put("id", bestMatch.getId());
  659. matchedRuleInfo.put("activityId", bestMatch.getActivityId());
  660. matchedRuleInfo.put("threshold", bestMatch.getThreshold());
  661. matchedRuleInfo.put("reduceAmount", bestMatch.getReduceAmount());
  662. if (bestActivity != null)
  663. {
  664. matchedRuleInfo.put("activityName", bestActivity.getName());
  665. }
  666. pathResult.put("matchedRule", matchedRuleInfo);
  667. }
  668. else
  669. {
  670. pathResult.put("subtotal", originalAmount);
  671. pathResult.put("promotionReduce", BigDecimal.ZERO);
  672. }
  673. return pathResult;
  674. }
  675. /**
  676. * 券减免计算结果: reduce=减免金额, items=商品券参与的商品明细(仅商品券有)
  677. */
  678. private static class CouponCalcResult
  679. {
  680. BigDecimal reduce = BigDecimal.ZERO;
  681. List<Map<String, Object>> items = new ArrayList<>();
  682. }
  683. /**
  684. * 计算单张券的减免金额; 商品券(couponType=2)同时返回参与商品明细(名称/原价/优惠后价)
  685. */
  686. private CouponCalcResult calcCouponReduce(PromotionCouponRule couponRule, PromotionCouponBatch batch,
  687. List<Map<String, Object>> itemsWithPrice,
  688. BigDecimal baseAmount, Map<Long, BigDecimal> priceMap,
  689. Map<Long, String> nameMap)
  690. {
  691. CouponCalcResult result = new CouponCalcResult();
  692. if (batch.getCouponType() != null && batch.getCouponType() == 2)
  693. {
  694. if (couponRule.getProductId() != null)
  695. {
  696. Long pid = couponRule.getProductId();
  697. BigDecimal productPrice = priceMap.getOrDefault(pid, BigDecimal.ZERO);
  698. int quantity = 0;
  699. for (Map<String, Object> item : itemsWithPrice)
  700. {
  701. if (pid.equals(toLong(item.get("productId"))))
  702. {
  703. quantity = toInt(item.get("quantity"));
  704. break;
  705. }
  706. }
  707. BigDecimal totalProductPrice = productPrice.multiply(BigDecimal.valueOf(quantity));
  708. BigDecimal finalLineTotal = totalProductPrice;
  709. if (couponRule.getDiscountRate() != null)
  710. {
  711. BigDecimal reduction = totalProductPrice.multiply(BigDecimal.ONE.subtract(couponRule.getDiscountRate()));
  712. result.reduce = reduction.max(BigDecimal.ZERO).min(totalProductPrice)
  713. .setScale(0, RoundingMode.HALF_UP);
  714. finalLineTotal = totalProductPrice.subtract(result.reduce).max(BigDecimal.ZERO)
  715. .setScale(0, RoundingMode.HALF_UP);
  716. }
  717. else if (couponRule.getAmount() != null)
  718. {
  719. result.reduce = couponRule.getAmount().min(totalProductPrice)
  720. .setScale(0, RoundingMode.HALF_UP);
  721. finalLineTotal = totalProductPrice.subtract(result.reduce).max(BigDecimal.ZERO)
  722. .setScale(0, RoundingMode.HALF_UP);
  723. }
  724. // 购物车含该商品才产出明细
  725. if (quantity > 0)
  726. {
  727. Map<String, Object> lineItem = new LinkedHashMap<>();
  728. lineItem.put("productId", pid);
  729. lineItem.put("quantity", quantity);
  730. lineItem.put("unitPrice", productPrice);
  731. lineItem.put("name", nameMap.getOrDefault(pid, ""));
  732. lineItem.put("originalLineTotal", totalProductPrice.setScale(0, RoundingMode.HALF_UP));
  733. lineItem.put("finalLineTotal", finalLineTotal);
  734. if (couponRule.getDiscountRate() != null)
  735. {
  736. lineItem.put("discountRate", couponRule.getDiscountRate());
  737. }
  738. result.items.add(lineItem);
  739. }
  740. }
  741. return result;
  742. }
  743. else
  744. {
  745. if (couponRule.getAmount() != null)
  746. {
  747. result.reduce = couponRule.getAmount().min(baseAmount).setScale(0, RoundingMode.HALF_UP);
  748. }
  749. return result;
  750. }
  751. }
  752. /**
  753. * 构建用户可用优惠券列表
  754. */
  755. private List<Map<String, Object>> buildAvailableCoupons(Long storeId, Long userId,
  756. BigDecimal originalAmount,
  757. BigDecimal afterPromotion,
  758. BigDecimal promotionReduce,
  759. List<Map<String, Object>> itemsWithPrice,
  760. Map<Long, List<PromotionActivityRule>> rulesByProduct,
  761. Map<Long, PromotionActivity> activityMap,
  762. Map<Long, BigDecimal> priceMap,
  763. Map<Long, String> nameMap)
  764. {
  765. List<Map<String, Object>> available = new ArrayList<>();
  766. if (userId == null)
  767. {
  768. return available;
  769. }
  770. LambdaQueryWrapper<PromotionUserCoupon> wrapper = new LambdaQueryWrapper<>();
  771. wrapper.eq(PromotionUserCoupon::getUserId, userId)
  772. .eq(PromotionUserCoupon::getStoreId, storeId)
  773. .eq(PromotionUserCoupon::getStatus, 0)
  774. .gt(PromotionUserCoupon::getExpireTime, new Date());
  775. List<PromotionUserCoupon> userCoupons = userCouponMapper.selectList(wrapper);
  776. for (PromotionUserCoupon uc : userCoupons)
  777. {
  778. PromotionCouponBatch batch = couponBatchMapper.selectById(uc.getBatchId());
  779. if (batch == null || batch.getStatus() == null || batch.getStatus() != 1)
  780. {
  781. continue;
  782. }
  783. PromotionCouponRule rule = couponRuleMapper.selectRuleByBatchId(uc.getBatchId());
  784. if (rule == null)
  785. {
  786. continue;
  787. }
  788. Map<String, Object> couponInfo = new LinkedHashMap<>();
  789. couponInfo.put("id", uc.getId());
  790. couponInfo.put("name", batch.getName());
  791. couponInfo.put("couponType", batch.getCouponType());
  792. couponInfo.put("isMutex", rule.getIsMutex());
  793. couponInfo.put("threshold", rule.getThreshold());
  794. couponInfo.put("amount", rule.getAmount());
  795. couponInfo.put("discountRate", rule.getDiscountRate());
  796. couponInfo.put("expireTime", uc.getExpireTime());
  797. boolean usable = true;
  798. int isMutex = rule.getIsMutex() != null ? rule.getIsMutex() : 0;
  799. if (isMutex == 1 && promotionReduce.compareTo(BigDecimal.ZERO) > 0)
  800. {
  801. couponInfo.put("conflictWithPromotion", true);
  802. }
  803. if (rule.getThreshold() != null)
  804. {
  805. BigDecimal baseForCheck = isMutex == 1 ? originalAmount : afterPromotion;
  806. if (baseForCheck.compareTo(rule.getThreshold()) < 0)
  807. {
  808. usable = false;
  809. }
  810. }
  811. if (batch.getCouponType() != null && batch.getCouponType() == 2 && rule.getProductId() != null)
  812. {
  813. // 预览: 该商品券作用于的商品能优惠多少(名称/原价/优惠后价)
  814. CouponCalcResult preview = calcCouponReduce(rule, batch, itemsWithPrice, afterPromotion, priceMap, nameMap);
  815. if (preview.reduce.compareTo(BigDecimal.ZERO) > 0)
  816. {
  817. couponInfo.put("couponPreviewReduce", preview.reduce);
  818. }
  819. if (!preview.items.isEmpty())
  820. {
  821. couponInfo.put("productPreview", preview.items);
  822. }
  823. boolean inCart = itemsWithPrice.stream()
  824. .anyMatch(i -> rule.getProductId().equals(toLong(i.get("productId"))));
  825. if (!inCart)
  826. {
  827. usable = false;
  828. }
  829. }
  830. couponInfo.put("usable", usable);
  831. available.add(couponInfo);
  832. }
  833. return available;
  834. }
  835. private Long toLong(Object obj)
  836. {
  837. if (obj == null)
  838. {
  839. return null;
  840. }
  841. if (obj instanceof Long)
  842. {
  843. return (Long) obj;
  844. }
  845. if (obj instanceof Number)
  846. {
  847. return ((Number) obj).longValue();
  848. }
  849. try
  850. {
  851. return Long.parseLong(obj.toString());
  852. }
  853. catch (NumberFormatException e)
  854. {
  855. return null;
  856. }
  857. }
  858. private int toInt(Object obj)
  859. {
  860. if (obj == null)
  861. {
  862. return 0;
  863. }
  864. if (obj instanceof Number)
  865. {
  866. return ((Number) obj).intValue();
  867. }
  868. try
  869. {
  870. return Integer.parseInt(obj.toString());
  871. }
  872. catch (NumberFormatException e)
  873. {
  874. return 0;
  875. }
  876. }
  877. }