|
|
@@ -0,0 +1,634 @@
|
|
|
+package com.fs.hisStore.service.impl;
|
|
|
+
|
|
|
+import cn.hutool.core.util.NumberUtil;
|
|
|
+import cn.hutool.core.util.ObjectUtil;
|
|
|
+import com.fs.common.exception.CustomException;
|
|
|
+import com.fs.common.core.redis.RedisCache;
|
|
|
+import com.fs.hisStore.domain.FsStoreOrderScrm;
|
|
|
+import com.fs.hisStore.domain.FsStoreProductCategoryScrm;
|
|
|
+import com.fs.hisStore.domain.FsStorePromotionActivity;
|
|
|
+import com.fs.hisStore.domain.FsStorePromotionScope;
|
|
|
+import com.fs.hisStore.domain.FsStorePromotionTier;
|
|
|
+import com.fs.hisStore.domain.FsStorePromotionUsage;
|
|
|
+import com.fs.hisStore.mapper.FsStorePromotionActivityMapper;
|
|
|
+import com.fs.hisStore.mapper.FsStorePromotionScopeMapper;
|
|
|
+import com.fs.hisStore.mapper.FsStorePromotionTierMapper;
|
|
|
+import com.fs.hisStore.mapper.FsStorePromotionUsageMapper;
|
|
|
+import com.fs.hisStore.dto.FsStorePromotionUsageCountDTO;
|
|
|
+import com.fs.hisStore.param.FsStorePromotionComputeParam;
|
|
|
+import com.fs.hisStore.param.FsStorePromotionListMultiStoreParam;
|
|
|
+import com.fs.hisStore.param.FsStorePromotionListParam;
|
|
|
+import com.fs.hisStore.service.IFsStoreProductCategoryScrmService;
|
|
|
+import com.fs.hisStore.service.IFsStorePromotionComputeService;
|
|
|
+import com.fs.hisStore.support.FsStorePromotionTierCalculator;
|
|
|
+import com.fs.hisStore.support.FsStorePromotionTierCalculator.EligibleSummary;
|
|
|
+import com.fs.hisStore.vo.FsStoreCartQueryVO;
|
|
|
+import com.fs.hisStore.vo.FsStorePromotionActivityItemVO;
|
|
|
+import com.fs.hisStore.vo.FsStorePromotionComputeResultVO;
|
|
|
+import com.fs.hisStore.vo.FsStorePromotionListVO;
|
|
|
+import com.fs.hisStore.vo.FsStorePromotionTierItemVO;
|
|
|
+import com.fs.hisStore.vo.FsStorePromotionTierMatchVO;
|
|
|
+import org.springframework.beans.factory.annotation.Autowired;
|
|
|
+import org.springframework.stereotype.Service;
|
|
|
+import org.springframework.util.CollectionUtils;
|
|
|
+
|
|
|
+import java.math.BigDecimal;
|
|
|
+import java.util.ArrayList;
|
|
|
+import java.util.Collections;
|
|
|
+import java.util.Comparator;
|
|
|
+import java.util.Date;
|
|
|
+import java.util.HashMap;
|
|
|
+import java.util.HashSet;
|
|
|
+import java.util.List;
|
|
|
+import java.util.Map;
|
|
|
+import java.util.Objects;
|
|
|
+import java.util.Set;
|
|
|
+import java.util.stream.Collectors;
|
|
|
+
|
|
|
+@Service
|
|
|
+public class FsStorePromotionComputeServiceImpl implements IFsStorePromotionComputeService {
|
|
|
+
|
|
|
+ private static final Map<Integer, String> SCOPE_TYPE_LABELS = new HashMap<>();
|
|
|
+ private static final Map<Integer, String> TIER_TYPE_LABELS = new HashMap<>();
|
|
|
+
|
|
|
+ static {
|
|
|
+ SCOPE_TYPE_LABELS.put(1, "全场通用");
|
|
|
+ SCOPE_TYPE_LABELS.put(2, "指定分类");
|
|
|
+ SCOPE_TYPE_LABELS.put(3, "指定商品");
|
|
|
+ TIER_TYPE_LABELS.put(1, "金额");
|
|
|
+ TIER_TYPE_LABELS.put(2, "折扣");
|
|
|
+ }
|
|
|
+
|
|
|
+ @Autowired
|
|
|
+ private RedisCache redisCache;
|
|
|
+ @Autowired
|
|
|
+ private FsStorePromotionActivityMapper activityMapper;
|
|
|
+ @Autowired
|
|
|
+ private FsStorePromotionTierMapper tierMapper;
|
|
|
+ @Autowired
|
|
|
+ private FsStorePromotionScopeMapper scopeMapper;
|
|
|
+ @Autowired
|
|
|
+ private FsStorePromotionUsageMapper usageMapper;
|
|
|
+ @Autowired
|
|
|
+ private IFsStoreProductCategoryScrmService categoryService;
|
|
|
+
|
|
|
+ @Override
|
|
|
+ public FsStorePromotionListVO listApplicablePromotions(Long userId, FsStorePromotionListParam param) {
|
|
|
+ List<FsStoreCartQueryVO> carts = loadCartsByOrderKey(param.getOrderKey());
|
|
|
+ Long storeId = resolveStoreId(carts, param.getStoreId());
|
|
|
+ if (storeId == null) {
|
|
|
+ throw new CustomException("无法识别店铺信息");
|
|
|
+ }
|
|
|
+ List<FsStoreCartQueryVO> storeCarts = filterCartsByStore(carts, storeId);
|
|
|
+ return buildPromotionListVO(userId, storeId, storeCarts, param.getCouponUserId());
|
|
|
+ }
|
|
|
+
|
|
|
+ @Override
|
|
|
+ public List<FsStorePromotionListVO> listApplicablePromotionsMultiStore(Long userId,
|
|
|
+ FsStorePromotionListMultiStoreParam param) {
|
|
|
+ List<FsStorePromotionListVO> result = new ArrayList<>();
|
|
|
+ for (String orderKey : param.getOrderKeys()) {
|
|
|
+ List<FsStoreCartQueryVO> carts = loadCartsByOrderKey(orderKey);
|
|
|
+ Long storeId = resolveStoreId(carts, null);
|
|
|
+ if (storeId == null) {
|
|
|
+ throw new CustomException("无法识别店铺信息");
|
|
|
+ }
|
|
|
+ result.add(buildPromotionListVO(userId, storeId, carts, param.getCouponUserId()));
|
|
|
+ }
|
|
|
+ return result;
|
|
|
+ }
|
|
|
+
|
|
|
+ @Override
|
|
|
+ public FsStorePromotionComputeResultVO computePromotion(Long userId, FsStorePromotionComputeParam param) {
|
|
|
+ List<FsStoreCartQueryVO> carts = loadCartsByOrderKey(param.getOrderKey());
|
|
|
+ return computePromotionWithCarts(userId, carts, param.getStoreId(),
|
|
|
+ param.getPromotionActivityId(), param.getCouponUserId());
|
|
|
+ }
|
|
|
+
|
|
|
+ @Override
|
|
|
+ public FsStorePromotionComputeResultVO computePromotionWithCarts(Long userId,
|
|
|
+ List<FsStoreCartQueryVO> carts,
|
|
|
+ Long storeId,
|
|
|
+ Long promotionActivityId,
|
|
|
+ Long couponUserId) {
|
|
|
+ if (promotionActivityId == null) {
|
|
|
+ return null;
|
|
|
+ }
|
|
|
+ if (CollectionUtils.isEmpty(carts)) {
|
|
|
+ throw new CustomException("购物车为空", 501);
|
|
|
+ }
|
|
|
+ Long resolvedStoreId = resolveStoreId(carts, storeId);
|
|
|
+ if (resolvedStoreId == null) {
|
|
|
+ throw new CustomException("无法识别店铺信息");
|
|
|
+ }
|
|
|
+ List<FsStoreCartQueryVO> storeCarts = filterCartsByStore(carts, resolvedStoreId);
|
|
|
+ activityMapper.expireActivities();
|
|
|
+
|
|
|
+ FsStorePromotionActivity activity = activityMapper.selectFsStorePromotionActivityById(promotionActivityId);
|
|
|
+ if (activity == null || activity.getIsDel() != null && activity.getIsDel() == 1) {
|
|
|
+ throw new CustomException("活动不存在");
|
|
|
+ }
|
|
|
+ validateActivityActive(activity, resolvedStoreId);
|
|
|
+
|
|
|
+ List<FsStorePromotionTier> tiers = tierMapper.selectByActivityId(activity.getId());
|
|
|
+ List<FsStorePromotionScope> scopes = loadScopesIfNeeded(activity);
|
|
|
+ Set<Long> scopeTargetIds = scopes.stream().map(FsStorePromotionScope::getTargetId).collect(Collectors.toSet());
|
|
|
+ Map<Long, FsStoreProductCategoryScrm> categoryMap = buildCategoryMap(storeCarts, scopes);
|
|
|
+
|
|
|
+ EligibleSummary summary = FsStorePromotionTierCalculator.computeEligibleSummary(
|
|
|
+ storeCarts, activity.getScopeType(), scopeTargetIds, categoryMap);
|
|
|
+ int usedCount = usageMapper.countEffectiveByActivityAndUser(activity.getId(), userId);
|
|
|
+ FsStorePromotionActivityItemVO item = buildActivityItem(
|
|
|
+ activity, tiers, summary, couponUserId, usedCount);
|
|
|
+
|
|
|
+ FsStorePromotionComputeResultVO result = new FsStorePromotionComputeResultVO();
|
|
|
+ result.setStoreId(resolvedStoreId);
|
|
|
+ result.setStoreName(resolveStoreName(storeCarts));
|
|
|
+ result.setPromotionActivityId(activity.getId());
|
|
|
+ result.setPromotionTitle(activity.getTitle());
|
|
|
+ result.setTierType(activity.getTierType());
|
|
|
+ result.setTierTypeLabel(resolveTierTypeLabel(activity.getTierType()));
|
|
|
+ result.setEligibleAmount(summary.getAmount());
|
|
|
+ result.setEligibleQuantity(summary.getQuantity());
|
|
|
+ result.setMatchedTier(item.getMatchedTier());
|
|
|
+ result.setNextTierTip(item.getNextTierTip());
|
|
|
+ result.setPromotionDiscountAmount(item.getEstimatedDiscount());
|
|
|
+ result.setEnabled(item.getEnabled());
|
|
|
+ result.setDisabledReason(item.getDisabledReason());
|
|
|
+ result.setPromotionRemainCount(item.getUserRemainCount());
|
|
|
+
|
|
|
+ if (Boolean.TRUE.equals(item.getEnabled()) && item.getMatchedTier() != null) {
|
|
|
+ result.setPromotionTierId(item.getMatchedTier().getTierId());
|
|
|
+ }
|
|
|
+
|
|
|
+ BigDecimal storeTotal = calculateStoreTotalAmount(storeCarts);
|
|
|
+ BigDecimal discount = item.getEstimatedDiscount() == null ? BigDecimal.ZERO : item.getEstimatedDiscount();
|
|
|
+ result.setPayAmountAfterPromotion(NumberUtil.sub(storeTotal, discount).max(BigDecimal.ZERO));
|
|
|
+ return result;
|
|
|
+ }
|
|
|
+
|
|
|
+ private List<FsStorePromotionScope> loadScopesIfNeeded(FsStorePromotionActivity activity) {
|
|
|
+ if (activity.getScopeType() == null || activity.getScopeType() == 1) {
|
|
|
+ return Collections.emptyList();
|
|
|
+ }
|
|
|
+ return scopeMapper.selectByActivityId(activity.getId());
|
|
|
+ }
|
|
|
+
|
|
|
+ private FsStorePromotionListVO buildPromotionListVO(Long userId,
|
|
|
+ Long storeId,
|
|
|
+ List<FsStoreCartQueryVO> storeCarts,
|
|
|
+ Long couponUserId) {
|
|
|
+ activityMapper.expireActivities();
|
|
|
+ List<FsStorePromotionActivity> activities = activityMapper.selectActivePromotionsByStoreId(storeId);
|
|
|
+
|
|
|
+ FsStorePromotionListVO vo = new FsStorePromotionListVO();
|
|
|
+ vo.setStoreId(storeId);
|
|
|
+ vo.setStoreName(resolveStoreName(storeCarts));
|
|
|
+ if (CollectionUtils.isEmpty(activities)) {
|
|
|
+ vo.setActivities(Collections.emptyList());
|
|
|
+ return vo;
|
|
|
+ }
|
|
|
+
|
|
|
+ PromotionBatchData batchData = loadPromotionBatchData(activities, userId);
|
|
|
+ Map<Long, FsStoreProductCategoryScrm> categoryMap = buildCategoryMap(
|
|
|
+ storeCarts, flattenScopes(batchData.getScopeMap()));
|
|
|
+ EligibleSummary storeSummary = summarizeStoreCarts(storeCarts);
|
|
|
+
|
|
|
+ List<FsStorePromotionActivityItemVO> activityItems = new ArrayList<>();
|
|
|
+ for (FsStorePromotionActivity activity : activities) {
|
|
|
+ List<FsStorePromotionTier> tiers = batchData.getTierMap().get(activity.getId());
|
|
|
+ if (CollectionUtils.isEmpty(tiers)) {
|
|
|
+ continue;
|
|
|
+ }
|
|
|
+ Set<Long> scopeTargetIds = resolveScopeTargetIds(batchData.getScopeMap(), activity.getId());
|
|
|
+ EligibleSummary summary = FsStorePromotionTierCalculator.computeEligibleSummary(
|
|
|
+ storeCarts, activity.getScopeType(), scopeTargetIds, categoryMap);
|
|
|
+ int usedCount = batchData.getUsageCountMap().getOrDefault(activity.getId(), 0);
|
|
|
+ activityItems.add(buildActivityItem(activity, tiers, summary, couponUserId, usedCount));
|
|
|
+ }
|
|
|
+
|
|
|
+ vo.setEligibleAmount(storeSummary.getAmount());
|
|
|
+ vo.setEligibleQuantity(storeSummary.getQuantity());
|
|
|
+ vo.setActivities(activityItems);
|
|
|
+ vo.setRecommendedActivityId(selectRecommendedActivityId(activityItems));
|
|
|
+ return vo;
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 单次请求内批量加载活动阶梯、适用范围、用户参与次数,避免循环查库。
|
|
|
+ */
|
|
|
+ private PromotionBatchData loadPromotionBatchData(List<FsStorePromotionActivity> activities, Long userId) {
|
|
|
+ List<Long> activityIds = activities.stream()
|
|
|
+ .map(FsStorePromotionActivity::getId)
|
|
|
+ .collect(Collectors.toList());
|
|
|
+ return new PromotionBatchData(
|
|
|
+ loadTierMap(activityIds),
|
|
|
+ loadScopeMap(activities),
|
|
|
+ loadUsageCountMap(activityIds, userId));
|
|
|
+ }
|
|
|
+
|
|
|
+ private Map<Long, List<FsStorePromotionTier>> loadTierMap(List<Long> activityIds) {
|
|
|
+ if (CollectionUtils.isEmpty(activityIds)) {
|
|
|
+ return Collections.emptyMap();
|
|
|
+ }
|
|
|
+ List<FsStorePromotionTier> tierList = tierMapper.selectByActivityIds(activityIds);
|
|
|
+ if (CollectionUtils.isEmpty(tierList)) {
|
|
|
+ return Collections.emptyMap();
|
|
|
+ }
|
|
|
+ Map<Long, List<FsStorePromotionTier>> tierMap = tierList.stream()
|
|
|
+ .collect(Collectors.groupingBy(FsStorePromotionTier::getActivityId));
|
|
|
+ tierMap.values().forEach(list -> list.sort(Comparator.comparing(
|
|
|
+ tier -> tier.getSortOrder() == null ? 0 : tier.getSortOrder())));
|
|
|
+ return tierMap;
|
|
|
+ }
|
|
|
+
|
|
|
+ private Map<Long, Integer> loadUsageCountMap(List<Long> activityIds, Long userId) {
|
|
|
+ if (userId == null || CollectionUtils.isEmpty(activityIds)) {
|
|
|
+ return Collections.emptyMap();
|
|
|
+ }
|
|
|
+ List<FsStorePromotionUsageCountDTO> countList =
|
|
|
+ usageMapper.countEffectiveByActivityIdsAndUser(activityIds, userId);
|
|
|
+ if (CollectionUtils.isEmpty(countList)) {
|
|
|
+ return Collections.emptyMap();
|
|
|
+ }
|
|
|
+ Map<Long, Integer> usageCountMap = new HashMap<>(countList.size());
|
|
|
+ for (FsStorePromotionUsageCountDTO item : countList) {
|
|
|
+ if (item.getActivityId() != null) {
|
|
|
+ usageCountMap.put(item.getActivityId(),
|
|
|
+ item.getUsedCount() == null ? 0 : item.getUsedCount());
|
|
|
+ }
|
|
|
+ }
|
|
|
+ return usageCountMap;
|
|
|
+ }
|
|
|
+
|
|
|
+ private Set<Long> resolveScopeTargetIds(Map<Long, List<FsStorePromotionScope>> scopeMap, Long activityId) {
|
|
|
+ return scopeMap.getOrDefault(activityId, Collections.emptyList()).stream()
|
|
|
+ .map(FsStorePromotionScope::getTargetId)
|
|
|
+ .filter(Objects::nonNull)
|
|
|
+ .collect(Collectors.toSet());
|
|
|
+ }
|
|
|
+
|
|
|
+ private boolean isStoreWideScope(Integer scopeType) {
|
|
|
+ return scopeType == null || scopeType == 1;
|
|
|
+ }
|
|
|
+
|
|
|
+ private static final class PromotionBatchData {
|
|
|
+ private final Map<Long, List<FsStorePromotionTier>> tierMap;
|
|
|
+ private final Map<Long, List<FsStorePromotionScope>> scopeMap;
|
|
|
+ private final Map<Long, Integer> usageCountMap;
|
|
|
+
|
|
|
+ private PromotionBatchData(Map<Long, List<FsStorePromotionTier>> tierMap,
|
|
|
+ Map<Long, List<FsStorePromotionScope>> scopeMap,
|
|
|
+ Map<Long, Integer> usageCountMap) {
|
|
|
+ this.tierMap = tierMap;
|
|
|
+ this.scopeMap = scopeMap;
|
|
|
+ this.usageCountMap = usageCountMap;
|
|
|
+ }
|
|
|
+
|
|
|
+ private Map<Long, List<FsStorePromotionTier>> getTierMap() {
|
|
|
+ return tierMap;
|
|
|
+ }
|
|
|
+
|
|
|
+ private Map<Long, List<FsStorePromotionScope>> getScopeMap() {
|
|
|
+ return scopeMap;
|
|
|
+ }
|
|
|
+
|
|
|
+ private Map<Long, Integer> getUsageCountMap() {
|
|
|
+ return usageCountMap;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ private FsStorePromotionActivityItemVO buildActivityItem(FsStorePromotionActivity activity,
|
|
|
+ List<FsStorePromotionTier> tiers,
|
|
|
+ EligibleSummary summary,
|
|
|
+ Long couponUserId,
|
|
|
+ int usedCount) {
|
|
|
+ FsStorePromotionActivityItemVO item = new FsStorePromotionActivityItemVO();
|
|
|
+ item.setActivityId(activity.getId());
|
|
|
+ item.setTitle(activity.getTitle());
|
|
|
+ item.setTierType(activity.getTierType());
|
|
|
+ item.setTierTypeLabel(resolveTierTypeLabel(activity.getTierType()));
|
|
|
+ item.setScopeType(activity.getScopeType());
|
|
|
+ item.setScopeTypeLabel(SCOPE_TYPE_LABELS.getOrDefault(activity.getScopeType(), ""));
|
|
|
+ item.setIsStackable(activity.getIsStackable() != null && activity.getIsStackable() == 1);
|
|
|
+ item.setIsCapped(activity.getIsCapped() != null && activity.getIsCapped() == 1);
|
|
|
+ item.setLimitPerUser(activity.getLimitPerUser());
|
|
|
+ item.setEligibleAmount(summary.getAmount());
|
|
|
+ item.setEligibleQuantity(summary.getQuantity());
|
|
|
+ item.setTiers(convertTierItems(tiers));
|
|
|
+
|
|
|
+ item.setUserUsedCount(usedCount);
|
|
|
+ item.setUserRemainCount(calcRemainCount(activity.getLimitPerUser(), usedCount));
|
|
|
+
|
|
|
+ String disabledReason = resolveDisabledReason(activity, summary, tiers, couponUserId, usedCount);
|
|
|
+ item.setDisabledReason(disabledReason);
|
|
|
+ item.setEnabled(disabledReason == null);
|
|
|
+
|
|
|
+ FsStorePromotionTier matchedTier = FsStorePromotionTierCalculator.matchTier(activity, summary, tiers);
|
|
|
+ if (matchedTier != null) {
|
|
|
+ item.setMatchedTier(toTierMatchVO(matchedTier));
|
|
|
+ item.setEstimatedDiscount(FsStorePromotionTierCalculator.calculateDiscount(activity, matchedTier, summary, tiers));
|
|
|
+ } else {
|
|
|
+ item.setEstimatedDiscount(BigDecimal.ZERO);
|
|
|
+ }
|
|
|
+ item.setNextTierTip(FsStorePromotionTierCalculator.buildNextTierTip(activity, summary, tiers));
|
|
|
+ return item;
|
|
|
+ }
|
|
|
+
|
|
|
+ private String resolveDisabledReason(FsStorePromotionActivity activity,
|
|
|
+ EligibleSummary summary,
|
|
|
+ List<FsStorePromotionTier> tiers,
|
|
|
+ Long couponUserId,
|
|
|
+ int usedCount) {
|
|
|
+ if (activity.getLimitPerUser() != null && activity.getLimitPerUser() > 0
|
|
|
+ && usedCount >= activity.getLimitPerUser()) {
|
|
|
+ return "您已超过该活动参与次数限制";
|
|
|
+ }
|
|
|
+ if (couponUserId != null && activity.getIsStackable() != null && activity.getIsStackable() == 0) {
|
|
|
+ return "该活动不可与优惠券叠加使用";
|
|
|
+ }
|
|
|
+ if (summary.getAmount().compareTo(BigDecimal.ZERO) <= 0 && summary.getQuantity() <= 0) {
|
|
|
+ return "购物车中无适用商品";
|
|
|
+ }
|
|
|
+ FsStorePromotionTier matchedTier = FsStorePromotionTierCalculator.matchTier(activity, summary, tiers);
|
|
|
+ if (matchedTier == null) {
|
|
|
+ int tierType = activity.getTierType() == null ? FsStorePromotionTierCalculator.TIER_TYPE_AMOUNT : activity.getTierType();
|
|
|
+ if (tierType == FsStorePromotionTierCalculator.TIER_TYPE_DISCOUNT) {
|
|
|
+ return "适用商品件数未达最低门槛";
|
|
|
+ }
|
|
|
+ return "适用商品金额未达最低门槛";
|
|
|
+ }
|
|
|
+ return null;
|
|
|
+ }
|
|
|
+
|
|
|
+ private Long selectRecommendedActivityId(List<FsStorePromotionActivityItemVO> activities) {
|
|
|
+ return activities.stream()
|
|
|
+ .filter(item -> Boolean.TRUE.equals(item.getEnabled()))
|
|
|
+ .filter(item -> item.getEstimatedDiscount() != null
|
|
|
+ && item.getEstimatedDiscount().compareTo(BigDecimal.ZERO) > 0)
|
|
|
+ .max(Comparator.comparing(FsStorePromotionActivityItemVO::getEstimatedDiscount))
|
|
|
+ .map(FsStorePromotionActivityItemVO::getActivityId)
|
|
|
+ .orElse(null);
|
|
|
+ }
|
|
|
+
|
|
|
+ private void validateActivityActive(FsStorePromotionActivity activity, Long storeId) {
|
|
|
+ Date now = new Date();
|
|
|
+ if (!Objects.equals(activity.getStoreId(), storeId)) {
|
|
|
+ throw new CustomException("活动与当前店铺不匹配");
|
|
|
+ }
|
|
|
+ if (activity.getManualStatus() == null || activity.getManualStatus() != 1) {
|
|
|
+ throw new CustomException("活动已结束,请重新下单");
|
|
|
+ }
|
|
|
+ if (activity.getStartTime() != null && activity.getStartTime().after(now)) {
|
|
|
+ throw new CustomException("活动未开始");
|
|
|
+ }
|
|
|
+ if (activity.getActivityEndTime() != null && activity.getActivityEndTime().before(now)) {
|
|
|
+ throw new CustomException("活动已结束,请重新下单");
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ private List<FsStoreCartQueryVO> loadCartsByOrderKey(String orderKey) {
|
|
|
+ String cartIds = redisCache.getCacheObject("orderKey:" + orderKey);
|
|
|
+ if (ObjectUtil.isNull(cartIds)) {
|
|
|
+ throw new CustomException("订单已过期", 501);
|
|
|
+ }
|
|
|
+ List<FsStoreCartQueryVO> carts = redisCache.getCacheObject("orderCarts:" + orderKey);
|
|
|
+ if (CollectionUtils.isEmpty(carts)) {
|
|
|
+ throw new CustomException("购物车为空", 501);
|
|
|
+ }
|
|
|
+ return carts;
|
|
|
+ }
|
|
|
+
|
|
|
+ private Long resolveStoreId(List<FsStoreCartQueryVO> carts, Long paramStoreId) {
|
|
|
+ if (paramStoreId != null) {
|
|
|
+ return paramStoreId;
|
|
|
+ }
|
|
|
+ return carts.stream()
|
|
|
+ .map(FsStoreCartQueryVO::getStoreId)
|
|
|
+ .filter(Objects::nonNull)
|
|
|
+ .findFirst()
|
|
|
+ .orElse(null);
|
|
|
+ }
|
|
|
+
|
|
|
+ private List<FsStoreCartQueryVO> filterCartsByStore(List<FsStoreCartQueryVO> carts, Long storeId) {
|
|
|
+ if (storeId == null) {
|
|
|
+ return carts;
|
|
|
+ }
|
|
|
+ return carts.stream()
|
|
|
+ .filter(cart -> storeId.equals(cart.getStoreId()))
|
|
|
+ .collect(Collectors.toList());
|
|
|
+ }
|
|
|
+
|
|
|
+ private String resolveStoreName(List<FsStoreCartQueryVO> carts) {
|
|
|
+ return carts.stream()
|
|
|
+ .map(FsStoreCartQueryVO::getStoreName)
|
|
|
+ .filter(Objects::nonNull)
|
|
|
+ .findFirst()
|
|
|
+ .orElse(null);
|
|
|
+ }
|
|
|
+
|
|
|
+ private BigDecimal calculateStoreTotalAmount(List<FsStoreCartQueryVO> carts) {
|
|
|
+ BigDecimal total = BigDecimal.ZERO;
|
|
|
+ for (FsStoreCartQueryVO cart : carts) {
|
|
|
+ if (cart.getCartNum() == null || cart.getPrice() == null) {
|
|
|
+ continue;
|
|
|
+ }
|
|
|
+ total = NumberUtil.add(total, NumberUtil.mul(cart.getCartNum(), cart.getPrice()));
|
|
|
+ }
|
|
|
+ return total;
|
|
|
+ }
|
|
|
+
|
|
|
+ private Integer calcRemainCount(Integer limitPerUser, int usedCount) {
|
|
|
+ if (limitPerUser == null || limitPerUser <= 0) {
|
|
|
+ return null;
|
|
|
+ }
|
|
|
+ return Math.max(limitPerUser - usedCount, 0);
|
|
|
+ }
|
|
|
+
|
|
|
+ private String resolveTierTypeLabel(Integer tierType) {
|
|
|
+ return TIER_TYPE_LABELS.getOrDefault(tierType == null ? 1 : tierType, "金额");
|
|
|
+ }
|
|
|
+
|
|
|
+ private List<FsStorePromotionTierItemVO> convertTierItems(List<FsStorePromotionTier> tiers) {
|
|
|
+ if (CollectionUtils.isEmpty(tiers)) {
|
|
|
+ return Collections.emptyList();
|
|
|
+ }
|
|
|
+ List<FsStorePromotionTierItemVO> items = new ArrayList<>();
|
|
|
+ for (FsStorePromotionTier tier : tiers) {
|
|
|
+ FsStorePromotionTierItemVO vo = new FsStorePromotionTierItemVO();
|
|
|
+ vo.setSortOrder(tier.getSortOrder());
|
|
|
+ vo.setThresholdAmount(tier.getThresholdAmount());
|
|
|
+ vo.setDiscountAmount(tier.getDiscountAmount());
|
|
|
+ items.add(vo);
|
|
|
+ }
|
|
|
+ return items;
|
|
|
+ }
|
|
|
+
|
|
|
+ private FsStorePromotionTierMatchVO toTierMatchVO(FsStorePromotionTier tier) {
|
|
|
+ FsStorePromotionTierMatchVO vo = new FsStorePromotionTierMatchVO();
|
|
|
+ vo.setTierId(tier.getId());
|
|
|
+ vo.setThresholdAmount(tier.getThresholdAmount());
|
|
|
+ vo.setDiscountAmount(tier.getDiscountAmount());
|
|
|
+ return vo;
|
|
|
+ }
|
|
|
+
|
|
|
+ private Map<Long, List<FsStorePromotionScope>> loadScopeMap(List<FsStorePromotionActivity> activities) {
|
|
|
+ Map<Long, List<FsStorePromotionScope>> scopeMap = new HashMap<>(activities.size());
|
|
|
+ List<Long> scopedActivityIds = new ArrayList<>();
|
|
|
+ for (FsStorePromotionActivity activity : activities) {
|
|
|
+ if (isStoreWideScope(activity.getScopeType())) {
|
|
|
+ scopeMap.put(activity.getId(), Collections.emptyList());
|
|
|
+ } else {
|
|
|
+ scopedActivityIds.add(activity.getId());
|
|
|
+ }
|
|
|
+ }
|
|
|
+ if (CollectionUtils.isEmpty(scopedActivityIds)) {
|
|
|
+ return scopeMap;
|
|
|
+ }
|
|
|
+ List<FsStorePromotionScope> scopeList = scopeMapper.selectByActivityIds(scopedActivityIds);
|
|
|
+ Map<Long, List<FsStorePromotionScope>> grouped = CollectionUtils.isEmpty(scopeList)
|
|
|
+ ? Collections.emptyMap()
|
|
|
+ : scopeList.stream().collect(Collectors.groupingBy(FsStorePromotionScope::getActivityId));
|
|
|
+ for (Long activityId : scopedActivityIds) {
|
|
|
+ scopeMap.put(activityId, grouped.getOrDefault(activityId, Collections.emptyList()));
|
|
|
+ }
|
|
|
+ return scopeMap;
|
|
|
+ }
|
|
|
+
|
|
|
+ private List<FsStorePromotionScope> flattenScopes(Map<Long, List<FsStorePromotionScope>> scopeMap) {
|
|
|
+ List<FsStorePromotionScope> scopes = new ArrayList<>();
|
|
|
+ for (List<FsStorePromotionScope> list : scopeMap.values()) {
|
|
|
+ scopes.addAll(list);
|
|
|
+ }
|
|
|
+ return scopes;
|
|
|
+ }
|
|
|
+
|
|
|
+ private Map<Long, FsStoreProductCategoryScrm> buildCategoryMap(List<FsStoreCartQueryVO> carts,
|
|
|
+ List<FsStorePromotionScope> scopes) {
|
|
|
+ Set<Long> cateIds = new HashSet<>();
|
|
|
+ for (FsStoreCartQueryVO cart : carts) {
|
|
|
+ if (cart.getCateId() != null) {
|
|
|
+ cateIds.add(cart.getCateId());
|
|
|
+ }
|
|
|
+ }
|
|
|
+ if (!CollectionUtils.isEmpty(scopes)) {
|
|
|
+ for (FsStorePromotionScope scope : scopes) {
|
|
|
+ if (scope.getScopeType() != null && scope.getScopeType() == 2 && scope.getTargetId() != null) {
|
|
|
+ cateIds.add(scope.getTargetId());
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+ if (cateIds.isEmpty()) {
|
|
|
+ return Collections.emptyMap();
|
|
|
+ }
|
|
|
+ List<FsStoreProductCategoryScrm> categories = categoryService.selectByCateIds(new ArrayList<>(cateIds));
|
|
|
+ if (CollectionUtils.isEmpty(categories)) {
|
|
|
+ return Collections.emptyMap();
|
|
|
+ }
|
|
|
+ return categories.stream()
|
|
|
+ .collect(Collectors.toMap(FsStoreProductCategoryScrm::getCateId, item -> item, (a, b) -> a));
|
|
|
+ }
|
|
|
+
|
|
|
+ private EligibleSummary summarizeStoreCarts(List<FsStoreCartQueryVO> storeCarts) {
|
|
|
+ EligibleSummary summary = new EligibleSummary();
|
|
|
+ summary.setAmount(calculateStoreTotalAmount(storeCarts));
|
|
|
+ int quantity = 0;
|
|
|
+ for (FsStoreCartQueryVO cart : storeCarts) {
|
|
|
+ if (cart.getCartNum() != null) {
|
|
|
+ quantity += cart.getCartNum();
|
|
|
+ }
|
|
|
+ }
|
|
|
+ summary.setQuantity(quantity);
|
|
|
+ return summary;
|
|
|
+ }
|
|
|
+
|
|
|
+ @Override
|
|
|
+ public void recordPendingUsage(FsStoreOrderScrm order, FsStorePromotionComputeResultVO promotion) {
|
|
|
+ if (order == null || order.getId() == null || promotion == null || promotion.getPromotionActivityId() == null) {
|
|
|
+ return;
|
|
|
+ }
|
|
|
+ BigDecimal discount = promotion.getPromotionDiscountAmount();
|
|
|
+ if (discount == null || discount.compareTo(BigDecimal.ZERO) <= 0) {
|
|
|
+ return;
|
|
|
+ }
|
|
|
+ FsStorePromotionUsage usage = new FsStorePromotionUsage();
|
|
|
+ usage.setActivityId(promotion.getPromotionActivityId());
|
|
|
+ usage.setUserId(order.getUserId());
|
|
|
+ usage.setOrderId(order.getId());
|
|
|
+ usage.setOrderAmount(order.getTotalPrice());
|
|
|
+ usage.setDiscountAmount(discount);
|
|
|
+ usage.setTierId(promotion.getPromotionTierId());
|
|
|
+ usage.setUsageStatus(0);
|
|
|
+ Date now = new Date();
|
|
|
+ usage.setUsageTime(now);
|
|
|
+ usage.setCreateTime(now);
|
|
|
+ usageMapper.insertFsStorePromotionUsage(usage);
|
|
|
+ }
|
|
|
+
|
|
|
+ @Override
|
|
|
+ public void confirmUsageByOrderId(Long orderId) {
|
|
|
+ if (orderId == null) {
|
|
|
+ return;
|
|
|
+ }
|
|
|
+ usageMapper.updateUsageStatusByOrderId(orderId, 1, 0);
|
|
|
+ }
|
|
|
+
|
|
|
+ @Override
|
|
|
+ public void rollbackUsageByOrderId(Long orderId) {
|
|
|
+ if (orderId == null) {
|
|
|
+ return;
|
|
|
+ }
|
|
|
+ usageMapper.updateUsageStatusByOrderId(orderId, 2, 0);
|
|
|
+ }
|
|
|
+
|
|
|
+ @Override
|
|
|
+ public FsStorePromotionComputeResultVO autoApplyBestPromotion(Long userId,
|
|
|
+ List<FsStoreCartQueryVO> carts,
|
|
|
+ Long storeId,
|
|
|
+ Long couponUserId) {
|
|
|
+ if (CollectionUtils.isEmpty(carts)) {
|
|
|
+ return null;
|
|
|
+ }
|
|
|
+ Long resolvedStoreId = resolveStoreId(carts, storeId);
|
|
|
+ if (resolvedStoreId == null) {
|
|
|
+ return null;
|
|
|
+ }
|
|
|
+ List<FsStoreCartQueryVO> storeCarts = filterCartsByStore(carts, resolvedStoreId);
|
|
|
+ FsStorePromotionListVO listVO = buildPromotionListVO(userId, resolvedStoreId, storeCarts, couponUserId);
|
|
|
+ Long bestId = listVO.getRecommendedActivityId();
|
|
|
+ if (bestId == null || CollectionUtils.isEmpty(listVO.getActivities())) {
|
|
|
+ return null;
|
|
|
+ }
|
|
|
+ FsStorePromotionActivityItemVO best = listVO.getActivities().stream()
|
|
|
+ .filter(item -> bestId.equals(item.getActivityId()))
|
|
|
+ .findFirst()
|
|
|
+ .orElse(null);
|
|
|
+ if (best == null || !Boolean.TRUE.equals(best.getEnabled())) {
|
|
|
+ return null;
|
|
|
+ }
|
|
|
+ BigDecimal discount = best.getEstimatedDiscount();
|
|
|
+ if (discount == null || discount.compareTo(BigDecimal.ZERO) <= 0) {
|
|
|
+ return null;
|
|
|
+ }
|
|
|
+ FsStorePromotionComputeResultVO result = new FsStorePromotionComputeResultVO();
|
|
|
+ result.setStoreId(resolvedStoreId);
|
|
|
+ result.setStoreName(listVO.getStoreName());
|
|
|
+ result.setPromotionActivityId(best.getActivityId());
|
|
|
+ result.setPromotionTitle(best.getTitle());
|
|
|
+ result.setTierType(best.getTierType());
|
|
|
+ result.setTierTypeLabel(best.getTierTypeLabel());
|
|
|
+ result.setEligibleAmount(best.getEligibleAmount());
|
|
|
+ result.setEligibleQuantity(best.getEligibleQuantity());
|
|
|
+ result.setMatchedTier(best.getMatchedTier());
|
|
|
+ result.setNextTierTip(best.getNextTierTip());
|
|
|
+ result.setPromotionDiscountAmount(discount);
|
|
|
+ result.setEnabled(true);
|
|
|
+ result.setPromotionRemainCount(best.getUserRemainCount());
|
|
|
+ if (best.getMatchedTier() != null) {
|
|
|
+ result.setPromotionTierId(best.getMatchedTier().getTierId());
|
|
|
+ }
|
|
|
+ BigDecimal storeTotal = calculateStoreTotalAmount(storeCarts);
|
|
|
+ result.setPayAmountAfterPromotion(NumberUtil.sub(storeTotal, discount).max(BigDecimal.ZERO));
|
|
|
+ return result;
|
|
|
+ }
|
|
|
+}
|