Browse Source

【功能】完成商品列表价格计算时,需要支持减免金额的功能

痴货 7 months ago
parent
commit
e44c0e668e
15 changed files with 350 additions and 50 deletions
  1. 5 5
      yudao-module-mall/yudao-module-product-biz/src/main/java/cn/iocoder/yudao/module/product/controller/app/spu/AppProductSpuController.java
  2. 2 2
      yudao-module-mall/yudao-module-product-biz/src/main/java/cn/iocoder/yudao/module/product/controller/app/spu/vo/AppProductSpuRespVO.java
  3. 9 0
      yudao-module-mall/yudao-module-promotion-api/src/main/java/cn/iocoder/yudao/module/promotion/api/discount/dto/DiscountProductRespDTO.java
  4. 10 0
      yudao-module-mall/yudao-module-promotion-api/src/main/java/cn/iocoder/yudao/module/promotion/api/reward/RewardActivityApi.java
  5. 1 0
      yudao-module-mall/yudao-module-promotion-api/src/main/java/cn/iocoder/yudao/module/promotion/enums/ErrorCodeConstants.java
  6. 8 0
      yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/api/reward/RewardActivityApiImpl.java
  7. 22 25
      yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/controller/app/activity/AppActivityController.java
  8. 41 2
      yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/dal/mysql/reward/RewardActivityMapper.java
  9. 11 0
      yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/service/reward/RewardActivityService.java
  10. 19 0
      yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/service/reward/RewardActivityServiceImpl.java
  11. 1 1
      yudao-module-mall/yudao-module-promotion-biz/src/main/resources/mapper/discount/DiscountProductMapper.xml
  12. 148 2
      yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/controller/app/order/AppTradeOrderController.java
  13. 59 0
      yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/controller/app/order/vo/AppTradeProductSettlementRespVO.java
  14. 3 2
      yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/service/price/calculator/TradeRewardActivityPriceCalculator.java
  15. 11 11
      yudao-server/src/main/resources/application-local.yaml

+ 5 - 5
yudao-module-mall/yudao-module-product-biz/src/main/java/cn/iocoder/yudao/module/product/controller/app/spu/AppProductSpuController.java

@@ -69,8 +69,8 @@ public class AppProductSpuController {
         list.forEach(spu -> spu.setSalesCount(spu.getSalesCount() + spu.getVirtualSalesCount()));
         List<AppProductSpuRespVO> voList = BeanUtils.toBean(list, AppProductSpuRespVO.class);
         // 处理 vip 价格
-        MemberLevelRespDTO memberLevel = getMemberLevel();
-        voList.forEach(vo -> vo.setVipPrice(calculateVipPrice(vo.getPrice(), memberLevel)));
+//        MemberLevelRespDTO memberLevel = getMemberLevel();
+//        voList.forEach(vo -> vo.setVipPrice(calculateVipPrice(vo.getPrice(), memberLevel)));
         return success(voList);
     }
 
@@ -86,8 +86,8 @@ public class AppProductSpuController {
         pageResult.getList().forEach(spu -> spu.setSalesCount(spu.getSalesCount() + spu.getVirtualSalesCount()));
         PageResult<AppProductSpuRespVO> voPageResult = BeanUtils.toBean(pageResult, AppProductSpuRespVO.class);
         // 处理 vip 价格
-        MemberLevelRespDTO memberLevel = getMemberLevel();
-        voPageResult.getList().forEach(vo -> vo.setVipPrice(calculateVipPrice(vo.getPrice(), memberLevel)));
+//        MemberLevelRespDTO memberLevel = getMemberLevel();
+//        voPageResult.getList().forEach(vo -> vo.setVipPrice(calculateVipPrice(vo.getPrice(), memberLevel)));
         return success(voPageResult);
     }
 
@@ -142,7 +142,7 @@ public class AppProductSpuController {
      */
     public Integer calculateVipPrice(Integer price, MemberLevelRespDTO memberLevel) {
         if (memberLevel == null || memberLevel.getDiscountPercent() == null) {
-            return 0;
+            return null;
         }
         Integer newPrice = price * memberLevel.getDiscountPercent() / 100;
         return price - newPrice;

+ 2 - 2
yudao-module-mall/yudao-module-product-biz/src/main/java/cn/iocoder/yudao/module/product/controller/app/spu/vo/AppProductSpuRespVO.java

@@ -38,8 +38,8 @@ public class AppProductSpuRespVO {
     @Schema(description = "市场价,单位使用:分", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024")
     private Integer marketPrice;
 
-    @Schema(description = "VIP 价格,单位使用:分", requiredMode = Schema.RequiredMode.REQUIRED, example = "968") // 通过会员等级,计算出折扣后价格
-    private Integer vipPrice;
+//    @Schema(description = "VIP 价格,单位使用:分", requiredMode = Schema.RequiredMode.REQUIRED, example = "968") // 通过会员等级,计算出折扣后价格
+//    private Integer vipPrice;
 
     @Schema(description = "库存", requiredMode = Schema.RequiredMode.REQUIRED, example = "666")
     private Integer stock;

+ 9 - 0
yudao-module-mall/yudao-module-promotion-api/src/main/java/cn/iocoder/yudao/module/promotion/api/discount/dto/DiscountProductRespDTO.java

@@ -2,6 +2,8 @@ package cn.iocoder.yudao.module.promotion.api.discount.dto;
 
 import lombok.Data;
 
+import java.time.LocalDateTime;
+
 /**
  * 限时折扣活动商品 Response DTO
  *
@@ -44,5 +46,12 @@ public class DiscountProductRespDTO {
      * 活动标题
      */
     private String activityName;
+    /**
+     * 活动结束时间点
+     *
+     * 冗余 {@link DiscountActivityDO#getEndTime()}
+     */
+    private LocalDateTime activityEndTime;
+
 
 }

+ 10 - 0
yudao-module-mall/yudao-module-promotion-api/src/main/java/cn/iocoder/yudao/module/promotion/api/reward/RewardActivityApi.java

@@ -3,6 +3,7 @@ package cn.iocoder.yudao.module.promotion.api.reward;
 import cn.iocoder.yudao.module.promotion.api.reward.dto.RewardActivityMatchRespDTO;
 
 import java.time.LocalDateTime;
+import java.util.Collection;
 import java.util.List;
 
 /**
@@ -21,4 +22,13 @@ public interface RewardActivityApi {
      */
     List<RewardActivityMatchRespDTO> getRewardActivityListByStatusAndNow(Integer status, LocalDateTime dateTime);
 
+    /**
+     * 获取指定 spu 编号最近参加的活动,每个 spuId 只返回一条记录
+     *
+     * @param spuIds   spu 编号
+     * @param status   状态
+     * @param dateTime 当前日期时间
+     * @return 满减送活动列表
+     */
+    List<RewardActivityMatchRespDTO> getRewardActivityBySpuIdsAndStatusAndDateTimeLt(Collection<Long> spuIds, Integer status, LocalDateTime dateTime);
 }

+ 1 - 0
yudao-module-mall/yudao-module-promotion-api/src/main/java/cn/iocoder/yudao/module/promotion/enums/ErrorCodeConstants.java

@@ -15,6 +15,7 @@ public interface ErrorCodeConstants {
     ErrorCode DISCOUNT_ACTIVITY_UPDATE_FAIL_STATUS_CLOSED = new ErrorCode(1_013_001_002, "限时折扣活动已关闭,不能修改");
     ErrorCode DISCOUNT_ACTIVITY_DELETE_FAIL_STATUS_NOT_CLOSED = new ErrorCode(1_013_001_003, "限时折扣活动未关闭,不能删除");
     ErrorCode DISCOUNT_ACTIVITY_CLOSE_FAIL_STATUS_CLOSED = new ErrorCode(1_013_001_004, "限时折扣活动已关闭,不能重复关闭");
+    ErrorCode DISCOUNT_ACTIVITY_TYPE_NOT_EXISTS = new ErrorCode(1_013_001_005, "限时折扣活动类型不存在");
 
     // ========== Banner 相关 1-013-002-000 ============
     ErrorCode BANNER_NOT_EXISTS = new ErrorCode(1_013_002_000, "Banner 不存在");

+ 8 - 0
yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/api/reward/RewardActivityApiImpl.java

@@ -2,6 +2,7 @@ package cn.iocoder.yudao.module.promotion.api.reward;
 
 import cn.iocoder.yudao.framework.common.util.object.BeanUtils;
 import cn.iocoder.yudao.module.promotion.api.reward.dto.RewardActivityMatchRespDTO;
+import cn.iocoder.yudao.module.promotion.convert.reward.RewardActivityConvert;
 import cn.iocoder.yudao.module.promotion.dal.dataobject.reward.RewardActivityDO;
 import cn.iocoder.yudao.module.promotion.service.reward.RewardActivityService;
 import jakarta.annotation.Resource;
@@ -9,6 +10,7 @@ import org.springframework.stereotype.Service;
 import org.springframework.validation.annotation.Validated;
 
 import java.time.LocalDateTime;
+import java.util.Collection;
 import java.util.List;
 
 /**
@@ -29,4 +31,10 @@ public class RewardActivityApiImpl implements RewardActivityApi {
         return BeanUtils.toBean(list, RewardActivityMatchRespDTO.class);
     }
 
+    @Override
+    public List<RewardActivityMatchRespDTO> getRewardActivityBySpuIdsAndStatusAndDateTimeLt(Collection<Long> spuIds, Integer status, LocalDateTime dateTime) {
+        List<RewardActivityDO> rewardActivityBySpuIdsAndStatusAndDateTimeLt = rewardActivityService.getRewardActivityBySpuIdsAndStatusAndDateTimeLt(spuIds, status, dateTime);
+        return RewardActivityConvert.INSTANCE.convertList(rewardActivityBySpuIdsAndStatusAndDateTimeLt);
+    }
+
 }

+ 22 - 25
yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/controller/app/activity/AppActivityController.java

@@ -33,6 +33,7 @@ import org.springframework.web.bind.annotation.RestController;
 
 import java.time.LocalDateTime;
 import java.util.*;
+import java.util.stream.Collectors;
 
 import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success;
 import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.*;
@@ -150,36 +151,32 @@ public class AppActivityController {
     }
 
     private void getRewardActivityList(Collection<Long> spuIds, LocalDateTime now, List<AppActivityRespVO> activityList) {
-        // 1.1 获得所有的活动
-        List<RewardActivityDO> rewardActivityList = rewardActivityService.getRewardActivityListByStatusAndDateTimeLt(
-                CommonStatusEnum.ENABLE.getStatus(), now);
+        // TODO @puhui999:有 3 范围,不只 spuId,还有 categoryId,全部,下次 fix
+        List<RewardActivityDO> rewardActivityList = rewardActivityService.getRewardActivityBySpuIdsAndStatusAndDateTimeLt(
+                spuIds, CommonStatusEnum.ENABLE.getStatus(), now);
         if (CollUtil.isEmpty(rewardActivityList)) {
             return;
         }
-        // 1.2 获得所有的商品信息
-        List<ProductSpuRespDTO> spuList = productSpuApi.getSpuList(spuIds);
-        if (CollUtil.isEmpty(spuList)) {
-            return;
-        }
 
-        // 2. 构建活动
-        for (RewardActivityDO rewardActivity : rewardActivityList) {
-            // 情况一:所有商品都能参加
-            if (PromotionProductScopeEnum.isAll(rewardActivity.getProductScope())) {
-                buildAppActivityRespVO(rewardActivity, spuIds, activityList);
-            }
-            // 情况二:指定商品参加
-            if (PromotionProductScopeEnum.isSpu(rewardActivity.getProductScope())) {
-                List<Long> fSpuIds = spuList.stream().map(ProductSpuRespDTO::getId).filter(id ->
-                        rewardActivity.getProductScopeValues().contains(id)).toList();
-                buildAppActivityRespVO(rewardActivity, fSpuIds, activityList);
-            }
-            // 情况三:指定商品类型参加
-            if (PromotionProductScopeEnum.isCategory(rewardActivity.getProductScope())) {
-                List<Long> fSpuIds = spuList.stream().filter(spuItem -> rewardActivity.getProductScopeValues()
-                        .contains(spuItem.getCategoryId())).map(ProductSpuRespDTO::getId).toList();
-                buildAppActivityRespVO(rewardActivity, fSpuIds, activityList);
+        Map<Long, Optional<RewardActivityDO>> spuIdAndActivityMap = spuIds.stream()
+                .collect(Collectors.toMap(
+                        spuId -> spuId,
+                        spuId -> rewardActivityList.stream()
+                                .filter(activity ->
+                                        ( activity.getProductScopeValues()!=null &&
+                                                (activity.getProductScopeValues().contains(spuId) ||
+                                                        activity.getProductScopeValues().contains(productSpuApi.getSpu(spuId).getCategoryId()))) ||
+                                                activity.getProductScope()==1
+                                )
+                                .max(Comparator.comparing(RewardActivityDO::getCreateTime))));
+        for (Long supId : spuIdAndActivityMap.keySet()) {
+            if (spuIdAndActivityMap.get(supId).isEmpty()) {
+                continue;
             }
+
+            RewardActivityDO rewardActivityDO = spuIdAndActivityMap.get(supId).get();
+            activityList.add(new AppActivityRespVO(rewardActivityDO.getId(), PromotionTypeEnum.REWARD_ACTIVITY.getType(),
+                    rewardActivityDO.getName(), supId, rewardActivityDO.getStartTime(), rewardActivityDO.getEndTime()));
         }
     }
 

+ 41 - 2
yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/dal/mysql/reward/RewardActivityMapper.java

@@ -1,14 +1,19 @@
 package cn.iocoder.yudao.module.promotion.dal.mysql.reward;
 
+import cn.hutool.core.util.StrUtil;
 import cn.iocoder.yudao.framework.common.pojo.PageResult;
 import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX;
 import cn.iocoder.yudao.framework.mybatis.core.query.LambdaQueryWrapperX;
 import cn.iocoder.yudao.module.promotion.controller.admin.reward.vo.RewardActivityPageReqVO;
 import cn.iocoder.yudao.module.promotion.dal.dataobject.reward.RewardActivityDO;
+import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
 import org.apache.ibatis.annotations.Mapper;
 
 import java.time.LocalDateTime;
+import java.util.Collection;
 import java.util.List;
+import java.util.function.Function;
+import java.util.stream.Collectors;
 
 /**
  * 满减送活动 Mapper
@@ -25,13 +30,47 @@ public interface RewardActivityMapper extends BaseMapperX<RewardActivityDO> {
                 .orderByDesc(RewardActivityDO::getId));
     }
 
+    default List<RewardActivityDO> selectListBySpuIdsAndStatus(Collection<Long> spuIds, Integer status) {
+        Function<Collection<Long>, String> productScopeValuesFindInSetFunc = ids -> ids.stream()
+                .map(id -> StrUtil.format("FIND_IN_SET({}, product_scope_values) ", id))
+                .collect(Collectors.joining(" OR "));
+        return selectList(new QueryWrapper<RewardActivityDO>()
+                .eq("status", status)
+                .apply(productScopeValuesFindInSetFunc.apply(spuIds)));
+    }
+
+    /**
+     * 获取指定活动编号的活动列表且
+     * 开始时间和结束时间小于给定时间 dateTime 的活动列表
+     *
+     * @param status   状态
+     * @param dateTime 指定日期
+     * @return 活动列表
+     */
     default List<RewardActivityDO> selectListByStatusAndDateTimeLt(Integer status, LocalDateTime dateTime) {
         return selectList(new LambdaQueryWrapperX<RewardActivityDO>()
                 .eq(RewardActivityDO::getStatus, status)
-                // 开始时间 < 指定时间(dateTime) < 结束时间,也就是说获取指定时间段的活动
-                .lt(RewardActivityDO::getStartTime, dateTime).gt(RewardActivityDO::getEndTime, dateTime)
+                .lt(RewardActivityDO::getStartTime, dateTime)
+                .gt(RewardActivityDO::getEndTime, dateTime)// 开始时间 < 指定时间 < 结束时间,也就是说获取指定时间段的活动
                 .orderByAsc(RewardActivityDO::getStartTime)
         );
     }
 
+    default List<RewardActivityDO> getRewardActivityByStatusAndDateTimeLt(Collection<Long> spuIds,Collection<Long> categoryIds, Integer status, LocalDateTime dateTime) {
+        //拼接通用券查询语句
+        Function<Collection<Long>, String> productScopeValuesFindInSetFunc = ids -> ids.stream()
+                .map(id -> StrUtil.format("FIND_IN_SET({}, product_scope_values) ", id))
+                .collect(Collectors.joining(" OR "));
+        return selectList(new LambdaQueryWrapperX<RewardActivityDO>()
+                .eq(RewardActivityDO::getStatus,status)
+                .lt(RewardActivityDO::getStartTime, dateTime)
+                .gt(RewardActivityDO::getEndTime, dateTime)
+                .and(i -> i. eq(RewardActivityDO::getProductScope, 2).and(i1 -> i1.apply(productScopeValuesFindInSetFunc.apply(spuIds))))
+                .or(i -> i.eq(RewardActivityDO::getProductScope, 1))
+                .or(i -> i. eq(RewardActivityDO::getProductScope, 3).and(i1 -> i1.apply(productScopeValuesFindInSetFunc.apply(categoryIds))))
+                .orderByDesc(RewardActivityDO::getId)
+                .last("limit 1")
+        );
+    }
+
 }

+ 11 - 0
yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/service/reward/RewardActivityService.java

@@ -8,6 +8,7 @@ import cn.iocoder.yudao.module.promotion.dal.dataobject.reward.RewardActivityDO;
 import jakarta.validation.Valid;
 
 import java.time.LocalDateTime;
+import java.util.Collection;
 import java.util.List;
 
 /**
@@ -71,4 +72,14 @@ public interface RewardActivityService {
      */
     List<RewardActivityDO> getRewardActivityListByStatusAndDateTimeLt(Integer status, LocalDateTime dateTime);
 
+    /**
+     * 获取指定 spu 编号最近参加的活动,每个 spuId 只返回一条记录
+     *
+     * @param spuIds   spu 编号
+     * @param status   状态
+     * @param dateTime 当前日期时间
+     * @return 满减送活动列表
+     */
+    List<RewardActivityDO> getRewardActivityBySpuIdsAndStatusAndDateTimeLt(Collection<Long> spuIds, Integer status, LocalDateTime dateTime);
+
 }

+ 19 - 0
yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/service/reward/RewardActivityServiceImpl.java

@@ -1,5 +1,6 @@
 package cn.iocoder.yudao.module.promotion.service.reward;
 
+import cn.hutool.core.collection.CollUtil;
 import cn.hutool.core.date.LocalDateTimeUtil;
 import cn.iocoder.yudao.framework.common.enums.CommonStatusEnum;
 import cn.iocoder.yudao.framework.common.pojo.PageResult;
@@ -22,8 +23,11 @@ import org.springframework.stereotype.Service;
 import org.springframework.validation.annotation.Validated;
 
 import java.time.LocalDateTime;
+import java.util.Collection;
+import java.util.Collections;
 import java.util.List;
 import java.util.Objects;
+import java.util.stream.Collectors;
 
 import static cn.hutool.core.collection.CollUtil.intersectionDistinct;
 import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception;
@@ -205,4 +209,19 @@ public class RewardActivityServiceImpl implements RewardActivityService {
         return rewardActivityMapper.selectListByStatusAndDateTimeLt(status, dateTime);
     }
 
+    @Override
+    public List<RewardActivityDO> getRewardActivityBySpuIdsAndStatusAndDateTimeLt(Collection<Long> spuIds, Integer status, LocalDateTime dateTime) {
+        List<ProductSpuRespDTO> spuList = productSpuApi.validateSpuList(spuIds);
+        //查询出商品的分类ids
+        List<Long> categoryIds = spuList.stream().map(ProductSpuRespDTO::getCategoryId).collect(Collectors.toList());
+        // 1. 查询出指定 spuId 的 spu 参加的活动
+        List<RewardActivityDO> rewardActivityList = rewardActivityMapper.getRewardActivityByStatusAndDateTimeLt(spuIds, categoryIds,status,dateTime);
+        if (CollUtil.isEmpty(rewardActivityList)) {
+            return Collections.emptyList();
+        }
+
+        // 2. 查询活动详情
+        return rewardActivityList;
+    }
+
 }

+ 1 - 1
yudao-module-mall/yudao-module-promotion-biz/src/main/resources/mapper/discount/DiscountProductMapper.xml

@@ -16,7 +16,7 @@
                 </foreach>
             </if>
             AND pda.start_time &lt;= CURRENT_TIME AND pda.end_time &gt;= CURRENT_TIME
-            AND pda.`status` = 20
+            AND pda.`status` = 0
             AND pda.deleted != 1
         </where>
     </select>

+ 148 - 2
yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/controller/app/order/AppTradeOrderController.java

@@ -1,9 +1,23 @@
 package cn.iocoder.yudao.module.trade.controller.app.order;
 
+import cn.iocoder.yudao.framework.common.enums.CommonStatusEnum;
 import cn.iocoder.yudao.framework.common.pojo.CommonResult;
 import cn.iocoder.yudao.framework.common.pojo.PageResult;
 import cn.iocoder.yudao.framework.security.core.annotations.PreAuthenticated;
+import cn.iocoder.yudao.module.member.api.level.MemberLevelApi;
+import cn.iocoder.yudao.module.member.api.level.dto.MemberLevelRespDTO;
+import cn.iocoder.yudao.module.member.api.user.MemberUserApi;
+import cn.iocoder.yudao.module.member.api.user.dto.MemberUserRespDTO;
 import cn.iocoder.yudao.module.pay.api.notify.dto.PayOrderNotifyReqDTO;
+import cn.iocoder.yudao.module.product.api.sku.ProductSkuApi;
+import cn.iocoder.yudao.module.product.api.sku.dto.ProductSkuRespDTO;
+import cn.iocoder.yudao.module.promotion.api.discount.DiscountActivityApi;
+import cn.iocoder.yudao.module.promotion.api.discount.dto.DiscountProductRespDTO;
+import cn.iocoder.yudao.module.promotion.api.reward.RewardActivityApi;
+import cn.iocoder.yudao.module.promotion.api.reward.dto.RewardActivityMatchRespDTO;
+import cn.iocoder.yudao.module.promotion.enums.common.PromotionConditionTypeEnum;
+import cn.iocoder.yudao.module.promotion.enums.common.PromotionDiscountTypeEnum;
+import cn.iocoder.yudao.module.promotion.enums.common.PromotionTypeEnum;
 import cn.iocoder.yudao.module.trade.controller.app.order.vo.*;
 import cn.iocoder.yudao.module.trade.controller.app.order.vo.item.AppTradeOrderItemCommentCreateReqVO;
 import cn.iocoder.yudao.module.trade.controller.app.order.vo.item.AppTradeOrderItemRespVO;
@@ -27,12 +41,14 @@ import lombok.extern.slf4j.Slf4j;
 import org.springframework.validation.annotation.Validated;
 import org.springframework.web.bind.annotation.*;
 
-import java.util.List;
-import java.util.Map;
+import java.time.LocalDateTime;
+import java.util.*;
 
+import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception;
 import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success;
 import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertSet;
 import static cn.iocoder.yudao.framework.security.core.util.SecurityFrameworkUtils.getLoginUserId;
+import static cn.iocoder.yudao.module.promotion.enums.ErrorCodeConstants.DISCOUNT_ACTIVITY_TYPE_NOT_EXISTS;
 
 @Tag(name = "用户 App - 交易订单")
 @RestController
@@ -54,6 +70,17 @@ public class AppTradeOrderController {
     @Resource
     private TradeOrderProperties tradeOrderProperties;
 
+    @Resource
+    private MemberLevelApi memberLevelApi;
+    @Resource
+    private MemberUserApi memberUserApi;
+    @Resource
+    private DiscountActivityApi discountActivityApi;
+    @Resource
+    private RewardActivityApi rewardActivityApi;
+    @Resource
+    private ProductSkuApi productKpuApi;
+
     @GetMapping("/settlement")
     @Operation(summary = "获得订单结算信息")
     @PreAuthenticated
@@ -61,6 +88,51 @@ public class AppTradeOrderController {
         return success(tradeOrderUpdateService.settlementOrder(getLoginUserId(), settlementReqVO));
     }
 
+    @GetMapping("/settlementProduct")
+    @Operation(summary = "获得商品结算信息")
+    public CommonResult<List<AppTradeProductSettlementRespVO>> settlementProduct(@RequestParam("ids") Set<Long> ids) {
+        List<AppTradeProductSettlementRespVO> appTradeProductSettlementRespVOS = new ArrayList<>();
+        MemberLevelRespDTO memberLevel = getMemberLevel();
+        ids.forEach(spuId -> {
+            List<AppTradeProductSettlementRespVO.Sku> skus = new ArrayList<>();
+            List<ProductSkuRespDTO> skuList = productKpuApi.getSkuListBySpuId(Collections.singletonList(spuId));
+            //查询sku的会员和限时优惠
+            skuList.forEach(sku -> {
+                //查询限时优惠价格
+                AppTradeProductSettlementRespVO.Sku skuDiscount = calculateDiscountPrice(sku.getId(), sku.getPrice());
+                if(skuDiscount != null){
+                    skus.add(skuDiscount);
+                }
+
+                //查询会员价
+                AppTradeProductSettlementRespVO.Sku skuVip = calculateVipPrice(sku.getId(), sku.getPrice(), memberLevel);
+                if(skuVip != null){
+                    skus.add(skuVip);
+                }
+            });
+            AppTradeProductSettlementRespVO.Reward reward = calculateReward(spuId);
+            AppTradeProductSettlementRespVO respVO = AppTradeProductSettlementRespVO.builder().id(spuId).skus(skus).build();
+            if(reward != null){
+                //创建满减活动对象
+                respVO.setReward(reward);
+            }
+            appTradeProductSettlementRespVOS.add(respVO);
+        });
+        return success(appTradeProductSettlementRespVOS);
+    }
+
+    private MemberLevelRespDTO getMemberLevel() {
+        Long userId = getLoginUserId();
+        if (userId == null) {
+            return null;
+        }
+        MemberUserRespDTO user = memberUserApi.getUser(userId);
+        if (user.getLevelId() == null || user.getLevelId() <= 0) {
+            return null;
+        }
+        return memberLevelApi.getMemberLevel(user.getLevelId());
+    }
+
     @PostMapping("/create")
     @Operation(summary = "创建订单")
     @PreAuthenticated
@@ -188,4 +260,78 @@ public class AppTradeOrderController {
         return success(tradeOrderUpdateService.createOrderItemCommentByMember(getLoginUserId(), createReqVO));
     }
 
+    /**
+     * 计算会员 VIP 优惠价格
+     *
+     * @param price 原价
+     * @param memberLevel 会员等级
+     * @return 优惠价格
+     */
+    public AppTradeProductSettlementRespVO.Sku calculateVipPrice(Long skuId, Integer price, MemberLevelRespDTO memberLevel) {
+        if (memberLevel == null || memberLevel.getDiscountPercent() == null) {
+            return null;
+        }
+        Integer newPrice = price * memberLevel.getDiscountPercent() / 100;
+        return AppTradeProductSettlementRespVO.Sku.builder().
+                skuId(skuId).
+                type(PromotionTypeEnum.MEMBER_LEVEL.getType()).
+                price(price - newPrice).build();
+    }
+
+    /**
+     * 计算限时优惠信息
+     *
+     * @param price 原价
+     * @param skuId 商品规格id
+     * @return 优惠价格
+     */
+    private AppTradeProductSettlementRespVO.Sku calculateDiscountPrice(Long skuId, Integer price) {
+        if (skuId == null) {
+            return null;
+        }
+
+        //根据商品id查询限时优惠
+        List<DiscountProductRespDTO> matchDiscountProductList = discountActivityApi.getMatchDiscountProductList(Collections.singletonList(skuId));
+        if (matchDiscountProductList != null && !matchDiscountProductList.isEmpty()) {
+            DiscountProductRespDTO discountProductRespDTO = matchDiscountProductList.get(matchDiscountProductList.size() - 1);
+            AppTradeProductSettlementRespVO.Sku sku = AppTradeProductSettlementRespVO.Sku.builder().
+                    skuId(skuId).
+                    discountId(discountProductRespDTO.getId()).
+                    type(PromotionTypeEnum.DISCOUNT_ACTIVITY.getType()).
+                    endTime(discountProductRespDTO.getActivityEndTime()).
+                    build();
+            Integer discountType = discountProductRespDTO.getDiscountType();
+            if(Objects.equals(PromotionDiscountTypeEnum.PRICE.getType(), discountType)){
+                sku.setPrice(price - discountProductRespDTO.getDiscountPrice() * 100);
+            }else if(Objects.equals(PromotionDiscountTypeEnum.PERCENT.getType(), discountType)){
+                Integer newPrice = price * discountProductRespDTO.getDiscountPercent() / 100;
+                sku.setPrice(price - newPrice);
+            }else{
+                throw exception(DISCOUNT_ACTIVITY_TYPE_NOT_EXISTS);
+            }
+            return sku;
+        }
+        return null;
+    }
+
+    /**
+     * 获取第一层满减活动
+     *
+     * @param spuId 商品规格id
+     * @return 优惠价格
+     */
+    private AppTradeProductSettlementRespVO.Reward calculateReward(Long spuId) {
+        List<RewardActivityMatchRespDTO> matchRewardActivityList = rewardActivityApi.getRewardActivityBySpuIdsAndStatusAndDateTimeLt(Collections.singletonList(spuId), CommonStatusEnum.ENABLE.getStatus(), LocalDateTime.now());
+        if(matchRewardActivityList != null && !matchRewardActivityList.isEmpty()){
+            RewardActivityMatchRespDTO rewardActivityMatchRespDTO = matchRewardActivityList.get(matchRewardActivityList.size() - 1);
+            if(rewardActivityMatchRespDTO != null){
+                RewardActivityMatchRespDTO.Rule rule = rewardActivityMatchRespDTO.getRules().get(0);
+                return AppTradeProductSettlementRespVO.Reward.builder().
+                        rewardActivity("满" + rule.getLimit() / 100 + (Objects.equals(rewardActivityMatchRespDTO.getConditionType(), PromotionConditionTypeEnum.PRICE.getType())?"元":"件"+"减") +rule.getDiscountPrice() / 100)
+                        .rewardId(rewardActivityMatchRespDTO.getId()).build();
+            }
+        }
+        return null;
+    }
+
 }

+ 59 - 0
yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/controller/app/order/vo/AppTradeProductSettlementRespVO.java

@@ -0,0 +1,59 @@
+package cn.iocoder.yudao.module.trade.controller.app.order.vo;
+
+import io.swagger.v3.oas.annotations.media.Schema;
+import lombok.Builder;
+import lombok.Data;
+
+import java.io.Serializable;
+import java.time.LocalDateTime;
+import java.util.List;
+
+@Schema(description = "用户 App - 商品结算信息 Response VO")
+@Data
+@Builder
+public class AppTradeProductSettlementRespVO {
+
+    @Schema(description = "商品编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
+    private Long id;
+
+    @Schema(description = "满减活动对象", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
+    private Reward reward;
+
+    @Schema(description = "sku活动信息", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
+    private List<Sku> skus;
+
+    /**
+     * 满减活动
+     */
+    @Data
+    @Builder
+    public static class Reward implements Serializable {
+        @Schema(description = "满减活动编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
+        private Long rewardId;
+
+        @Schema(description = "满减活动信息", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
+        private String rewardActivity;
+    }
+
+    /**
+     * SKU 数组
+     */
+    @Data
+    @Builder
+    public static class Sku implements Serializable {
+        @Schema(description = "商品 SKU 编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
+        private Long skuId;
+
+        @Schema(description = "价格", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
+        private Integer price;
+
+        @Schema(description = "营销类型", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") //PromotionTypeEnum
+        private Integer type;
+
+        @Schema(description = "限时优惠id", requiredMode = Schema.RequiredMode.REQUIRED)
+        private Long discountId;
+
+        @Schema(description = "活动结束时间", requiredMode = Schema.RequiredMode.REQUIRED)
+        private LocalDateTime endTime;
+    }
+}

+ 3 - 2
yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/service/price/calculator/TradeRewardActivityPriceCalculator.java

@@ -23,6 +23,7 @@ import java.util.Comparator;
 import java.util.List;
 import java.util.Map;
 
+import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertSet;
 import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.filterList;
 import static cn.iocoder.yudao.module.trade.service.price.calculator.TradePriceCalculatorHelper.formatPrice;
 
@@ -47,8 +48,8 @@ public class TradeRewardActivityPriceCalculator implements TradePriceCalculator
             return;
         }
         // 获得 SKU 对应的满减送活动
-        List<RewardActivityMatchRespDTO> rewardActivities = rewardActivityApi.getRewardActivityListByStatusAndNow(
-                CommonStatusEnum.ENABLE.getStatus(), LocalDateTime.now());
+        List<RewardActivityMatchRespDTO> rewardActivities = rewardActivityApi.getRewardActivityBySpuIdsAndStatusAndDateTimeLt(
+                convertSet(result.getItems(), TradePriceCalculateRespBO.OrderItem::getSpuId), CommonStatusEnum.ENABLE.getStatus(), LocalDateTime.now());
         if (CollUtil.isEmpty(rewardActivities)) {
             return;
         }

+ 11 - 11
yudao-server/src/main/resources/application-local.yaml

@@ -6,7 +6,7 @@ spring:
   # 数据源配置项
   autoconfigure:
     exclude:
-      - org.springframework.boot.autoconfigure.quartz.QuartzAutoConfiguration # 默认 local 环境,不开启 Quartz 的自动配置
+      #- org.springframework.boot.autoconfigure.quartz.QuartzAutoConfiguration # 默认 local 环境,不开启 Quartz 的自动配置
       - de.codecentric.boot.admin.server.config.AdminServerAutoConfiguration # 禁用 Spring Boot Admin 的 Server 的自动配置
       - de.codecentric.boot.admin.server.ui.config.AdminServerUiAutoConfiguration # 禁用 Spring Boot Admin 的 Server UI 的自动配置
       - de.codecentric.boot.admin.client.config.SpringBootAdminClientAutoConfiguration # 禁用 Spring Boot Admin 的 Client 的自动配置
@@ -45,7 +45,7 @@ spring:
       primary: master
       datasource:
         master:
-          url: jdbc:mysql://127.0.0.1:3306/ruoyi-vue-pro?useSSL=false&serverTimezone=Asia/Shanghai&allowPublicKeyRetrieval=true&nullCatalogMeansCurrent=true&rewriteBatchedStatements=true # MySQL Connector/J 8.X 连接的示例
+          url: jdbc:mysql://192.168.10.207:3306/specialty?useSSL=false&serverTimezone=Asia/Shanghai&allowPublicKeyRetrieval=true&nullCatalogMeansCurrent=true&rewriteBatchedStatements=true # MySQL Connector/J 8.X 连接的示例
           #          url: jdbc:mysql://127.0.0.1:3306/ruoyi-vue-pro?useSSL=true&allowPublicKeyRetrieval=true&useUnicode=true&characterEncoding=UTF-8&serverTimezone=Asia/Shanghai&rewriteBatchedStatements=true # MySQL Connector/J 5.X 连接的示例
           #          url: jdbc:postgresql://127.0.0.1:5432/ruoyi-vue-pro # PostgreSQL 连接的示例
           #          url: jdbc:oracle:thin:@127.0.0.1:1521:xe # Oracle 连接的示例
@@ -61,19 +61,19 @@ spring:
           #          password: SYSDBA001 # DM 连接的示例
           #          username: root # OpenGauss 连接的示例
           #          password: Yudao@2024 # OpenGauss 连接的示例
-        slave: # 模拟从库,可根据自己需要修改
-          lazy: true # 开启懒加载,保证启动速度
-          url: jdbc:mysql://127.0.0.1:3306/ruoyi-vue-pro?useSSL=false&serverTimezone=Asia/Shanghai&allowPublicKeyRetrieval=true&rewriteBatchedStatements=true&nullCatalogMeansCurrent=true
-          username: root
-          password: 123456
+#        slave: # 模拟从库,可根据自己需要修改
+#          lazy: true # 开启懒加载,保证启动速度
+#          url: jdbc:mysql://192.168.10.207:3306/ruoyi-vue-pro?useSSL=false&serverTimezone=Asia/Shanghai&allowPublicKeyRetrieval=true&rewriteBatchedStatements=true&nullCatalogMeansCurrent=true
+#          username: root
+#          password: 123456
 
   # Redis 配置。Redisson 默认的配置足够使用,一般不需要进行调优
   data:
     redis:
-      host: 127.0.0.1 # 地址
+      host: 192.168.10.207 # 地址
       port: 6379 # 端口
       database: 0 # 数据库索引
-#    password: dev # 密码,建议生产环境开启
+      password: 123456 # 密码,建议生产环境开启
 
 --- #################### 定时任务相关配置 ####################
 
@@ -200,8 +200,8 @@ wx:
 #    secret: 6f270509224a7ae1296bbf1c8cb97aed
 #    appid: wxc4598c446f8a9cb3 # 测试号(Kongdy 提供的)
 #    secret: 4a1a04e07f6a4a0751b39c3064a92c8b
-    appid: wx66186af0759f47c9 # 测试号(puhui 提供的)
-    secret: 3218bcbd112cbc614c7264ceb20144ac
+    appid: wx9a0a5b259d852380 # 测试号(puhui 提供的)
+    secret: 70e65fa9d1a4f2c4e1b2aa8751d3b75e
     config-storage:
       type: RedisTemplate # 采用 RedisTemplate 操作 Redis,会自动从 Spring 中获取
       key-prefix: wa # Redis Key 的前缀