浏览代码

Merge branch 'feature/mall_product' of https://gitee.com/zhijiantianya/ruoyi-vue-pro

YunaiV 1 年之前
父节点
当前提交
8f86f65884
共有 43 个文件被更改,包括 530 次插入74 次删除
  1. 1 1
      yudao-framework/yudao-common/src/main/java/cn/iocoder/yudao/framework/common/validation/InEnum.java
  2. 42 0
      yudao-framework/yudao-common/src/main/java/cn/iocoder/yudao/framework/common/validation/InEnumCollectionValidator.java
  3. 7 0
      yudao-module-mall/yudao-module-promotion-api/src/main/java/cn/iocoder/yudao/module/promotion/api/coupon/CouponApi.java
  4. 2 2
      yudao-module-mall/yudao-module-promotion-api/src/main/java/cn/iocoder/yudao/module/promotion/api/coupon/dto/CouponRespDTO.java
  5. 6 1
      yudao-module-mall/yudao-module-promotion-api/src/main/java/cn/iocoder/yudao/module/promotion/enums/ErrorCodeConstants.java
  6. 3 2
      yudao-module-mall/yudao-module-promotion-api/src/main/java/cn/iocoder/yudao/module/promotion/enums/common/PromotionProductScopeEnum.java
  7. 3 2
      yudao-module-mall/yudao-module-promotion-api/src/main/java/cn/iocoder/yudao/module/promotion/enums/coupon/CouponTakeTypeEnum.java
  8. 5 0
      yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/api/coupon/CouponApiImpl.java
  9. 11 3
      yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/controller/admin/coupon/CouponController.java
  10. 0 1
      yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/controller/admin/coupon/CouponTemplateController.java
  11. 3 3
      yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/controller/admin/coupon/vo/coupon/CouponBaseVO.java
  12. 24 0
      yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/controller/admin/coupon/vo/coupon/CouponSendReqVO.java
  13. 5 5
      yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/controller/admin/coupon/vo/template/CouponTemplateBaseVO.java
  14. 7 0
      yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/controller/admin/coupon/vo/template/CouponTemplatePageReqVO.java
  15. 2 2
      yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/controller/app/coupon/vo/template/AppCouponTemplateRespVO.java
  16. 28 0
      yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/convert/coupon/CouponConvert.java
  17. 4 4
      yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/dal/dataobject/coupon/CouponDO.java
  18. 2 2
      yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/dal/dataobject/coupon/CouponTemplateDO.java
  19. 7 0
      yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/dal/mysql/coupon/CouponMapper.java
  20. 18 0
      yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/dal/mysql/coupon/CouponTemplateMapper.java
  21. 49 0
      yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/service/coupon/CouponService.java
  22. 103 0
      yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/service/coupon/CouponServiceImpl.java
  23. 2 2
      yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/service/coupon/CouponTemplateService.java
  24. 2 1
      yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/service/coupon/CouponTemplateServiceImpl.java
  25. 1 0
      yudao-module-mall/yudao-module-trade-api/src/main/java/cn/iocoder/yudao/module/trade/enums/ErrorCodeConstants.java
  26. 20 0
      yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/controller/admin/aftersale/TradeAfterSaleController.java
  27. 2 10
      yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/controller/admin/order/TradeOrderController.java
  28. 22 0
      yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/controller/admin/order/vo/TradeOrderDetailRespVO.java
  29. 1 1
      yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/controller/admin/order/vo/TradeOrderPageReqVO.java
  30. 2 0
      yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/controller/app/delivery/AppDeliverPickUpStoreController.java
  31. 4 5
      yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/controller/app/order/AppTradeOrderController.java
  32. 4 2
      yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/convert/aftersale/TradeAfterSaleConvert.java
  33. 20 3
      yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/convert/order/TradeOrderConvert.java
  34. 2 0
      yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/dal/dataobject/order/TradeOrderItemDO.java
  35. 2 0
      yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/service/aftersale/TradeAfterSaleServiceImpl.java
  36. 9 1
      yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/service/order/TradeOrderQueryService.java
  37. 21 0
      yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/service/order/TradeOrderQueryServiceImpl.java
  38. 7 0
      yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/service/order/TradeOrderUpdateService.java
  39. 70 19
      yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/service/order/TradeOrderUpdateServiceImpl.java
  40. 1 0
      yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/service/price/TradePriceServiceImpl.java
  41. 4 1
      yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/service/price/calculator/TradeCouponPriceCalculator.java
  42. 1 0
      yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/service/price/calculator/TradeDeliveryPriceCalculator.java
  43. 1 1
      yudao-module-mall/yudao-module-trade-biz/src/test/java/cn/iocoder/yudao/module/trade/service/price/calculator/TradeCouponPriceCalculatorTest.java

+ 1 - 1
yudao-framework/yudao-common/src/main/java/cn/iocoder/yudao/framework/common/validation/InEnum.java

@@ -17,7 +17,7 @@ import java.lang.annotation.*;
 @Retention(RetentionPolicy.RUNTIME)
 @Documented
 @Constraint(
-        validatedBy = InEnumValidator.class
+        validatedBy = {InEnumValidator.class, InEnumCollectionValidator.class}
 )
 public @interface InEnum {
 

+ 42 - 0
yudao-framework/yudao-common/src/main/java/cn/iocoder/yudao/framework/common/validation/InEnumCollectionValidator.java

@@ -0,0 +1,42 @@
+package cn.iocoder.yudao.framework.common.validation;
+
+import cn.hutool.core.collection.CollUtil;
+import cn.iocoder.yudao.framework.common.core.IntArrayValuable;
+
+import javax.validation.ConstraintValidator;
+import javax.validation.ConstraintValidatorContext;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.List;
+import java.util.stream.Collectors;
+
+public class InEnumCollectionValidator implements ConstraintValidator<InEnum, Collection<Integer>> {
+
+    private List<Integer> values;
+
+    @Override
+    public void initialize(InEnum annotation) {
+        IntArrayValuable[] values = annotation.value().getEnumConstants();
+        if (values.length == 0) {
+            this.values = Collections.emptyList();
+        } else {
+            this.values = Arrays.stream(values[0].array()).boxed().collect(Collectors.toList());
+        }
+    }
+
+    @Override
+    public boolean isValid(Collection<Integer> list, ConstraintValidatorContext context) {
+        // 校验通过
+        if (CollUtil.containsAll(values, list)) {
+            return true;
+        }
+        // 校验不通过,自定义提示语句(因为,注解上的 value 是枚举类,无法获得枚举类的实际值)
+        context.disableDefaultConstraintViolation(); // 禁用默认的 message 的值
+        context.buildConstraintViolationWithTemplate(context.getDefaultConstraintMessageTemplate()
+                .replaceAll("\\{value}", CollUtil.join(list, ","))).addConstraintViolation(); // 重新添加错误提示语句
+        return false;
+    }
+
+}
+

+ 7 - 0
yudao-module-mall/yudao-module-promotion-api/src/main/java/cn/iocoder/yudao/module/promotion/api/coupon/CouponApi.java

@@ -20,6 +20,13 @@ public interface CouponApi {
      */
     void useCoupon(@Valid CouponUseReqDTO useReqDTO);
 
+    /**
+     * 退还已使用的优惠券
+     *
+     * @param id 优惠券编号
+     */
+    void returnUsedCoupon(Long id);
+
     /**
      * 校验优惠劵
      *

+ 2 - 2
yudao-module-mall/yudao-module-promotion-api/src/main/java/cn/iocoder/yudao/module/promotion/api/coupon/dto/CouponRespDTO.java

@@ -71,9 +71,9 @@ public class CouponRespDTO {
      */
     private Integer productScope;
     /**
-     * 商品 SPU 编号的数组
+     * 商品范围编号的数组
      */
-    private List<Long> productSpuIds;
+    private List<Long> productScopeValues;
     // ========== 使用规则 END ==========
 
     // ========== 使用效果 BEGIN ==========

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

@@ -27,12 +27,17 @@ public interface ErrorCodeConstants {
     // ========== 优惠劵模板 1013004000 ==========
     ErrorCode COUPON_TEMPLATE_NOT_EXISTS = new ErrorCode(1013004000, "优惠劵模板不存在");
     ErrorCode COUPON_TEMPLATE_TOTAL_COUNT_TOO_SMALL = new ErrorCode(1013004001, "发放数量不能小于已领取数量({})");
+    ErrorCode COUPON_TEMPLATE_NOT_ENOUGH = new ErrorCode(1013004002, "当前剩余数量不够领取");
+    ErrorCode COUPON_TEMPLATE_USER_ALREADY_TAKE = new ErrorCode(1013004003, "用户已领取过此优惠券");
+    ErrorCode COUPON_TEMPLATE_EXPIRED = new ErrorCode(1013004004, "优惠券已过期");
+    ErrorCode COUPON_TEMPLATE_CANNOT_TAKE = new ErrorCode(1013004005, "领取方式不正确");
 
-    // ========== 优惠劵模板 1013005000 ==========
+    // ========== 优惠劵 1013005000 ==========
     ErrorCode COUPON_NOT_EXISTS = new ErrorCode(1013005000, "优惠券不存在");
     ErrorCode COUPON_DELETE_FAIL_USED = new ErrorCode(1013005001, "回收优惠劵失败,优惠劵已被使用");
     ErrorCode COUPON_STATUS_NOT_UNUSED = new ErrorCode(1013005002, "优惠劵不处于待使用状态");
     ErrorCode COUPON_VALID_TIME_NOT_NOW = new ErrorCode(1013005003, "优惠券不在使用时间范围内");
+    ErrorCode COUPON_STATUS_NOT_USED = new ErrorCode(1013005004, "优惠劵不是已使用状态");
 
     // ========== 满减送活动 1013006000 ==========
     ErrorCode REWARD_ACTIVITY_NOT_EXISTS = new ErrorCode(1013006000, "满减送活动不存在");

+ 3 - 2
yudao-module-mall/yudao-module-promotion-api/src/main/java/cn/iocoder/yudao/module/promotion/enums/common/PromotionProductScopeEnum.java

@@ -15,8 +15,9 @@ import java.util.Arrays;
 @AllArgsConstructor
 public enum PromotionProductScopeEnum implements IntArrayValuable {
 
-    ALL(1, "全部商品参与"),
-    SPU(2, "指定商品参与"),
+    ALL(1, "通用卷"), // 全部商品
+    SPU(2, "商品卷"), // 指定商品
+    CATEGORY(3, "品类卷"), // 指定商品
     ;
 
     public static final int[] ARRAYS = Arrays.stream(values()).mapToInt(PromotionProductScopeEnum::getScope).toArray();

+ 3 - 2
yudao-module-mall/yudao-module-promotion-api/src/main/java/cn/iocoder/yudao/module/promotion/enums/coupon/CouponTakeTypeEnum.java

@@ -15,8 +15,9 @@ import java.util.Arrays;
 @Getter
 public enum CouponTakeTypeEnum implements IntArrayValuable {
 
-    BY_USER(1, "直接领取"), // 用户可在首页、每日领劵直接领取
-    BY_ADMIN(2, "指定发放"), // 后台指定会员赠送优惠劵
+    USER(1, "直接领取"), // 用户可在首页、每日领劵直接领取
+    ADMIN(2, "指定发放"), // 后台指定会员赠送优惠劵
+    REGISTER(3, "新人券"), // 注册时自动领取
     ;
 
     public static final int[] ARRAYS = Arrays.stream(values()).mapToInt(CouponTakeTypeEnum::getValue).toArray();

+ 5 - 0
yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/api/coupon/CouponApiImpl.java

@@ -28,6 +28,11 @@ public class CouponApiImpl implements CouponApi {
                 useReqDTO.getOrderId());
     }
 
+    @Override
+    public void returnUsedCoupon(Long id) {
+        couponService.returnUsedCoupon(id);
+    }
+
     @Override
     public CouponRespDTO validateCoupon(CouponValidReqDTO validReqDTO) {
         CouponDO coupon = couponService.validCoupon(validReqDTO.getId(), validReqDTO.getUserId());

+ 11 - 3
yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/controller/admin/coupon/CouponController.java

@@ -8,6 +8,7 @@ import cn.iocoder.yudao.module.member.api.user.MemberUserApi;
 import cn.iocoder.yudao.module.member.api.user.dto.MemberUserRespDTO;
 import cn.iocoder.yudao.module.promotion.controller.admin.coupon.vo.coupon.CouponPageItemRespVO;
 import cn.iocoder.yudao.module.promotion.controller.admin.coupon.vo.coupon.CouponPageReqVO;
+import cn.iocoder.yudao.module.promotion.controller.admin.coupon.vo.coupon.CouponSendReqVO;
 import cn.iocoder.yudao.module.promotion.convert.coupon.CouponConvert;
 import cn.iocoder.yudao.module.promotion.dal.dataobject.coupon.CouponDO;
 import cn.iocoder.yudao.module.promotion.service.coupon.CouponService;
@@ -21,7 +22,6 @@ import org.springframework.web.bind.annotation.*;
 import javax.annotation.Resource;
 import javax.validation.Valid;
 import java.util.Map;
-import java.util.Set;
 
 import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success;
 import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertSet;
@@ -64,12 +64,20 @@ public class CouponController {
         if (CollUtil.isEmpty(pageResulVO.getList())) {
             return success(pageResulVO);
         }
+
         // 读取用户信息,进行拼接
-        Set<Long> userIds = convertSet(pageResult.getList(), CouponDO::getUserId);
-        Map<Long, MemberUserRespDTO> userMap = memberUserApi.getUserMap(userIds);
+        Map<Long, MemberUserRespDTO> userMap = memberUserApi.getUserMap(convertSet(pageResult.getList(), CouponDO::getUserId));
         pageResulVO.getList().forEach(itemRespVO -> MapUtils.findAndThen(userMap, itemRespVO.getUserId(),
                 userRespDTO -> itemRespVO.setNickname(userRespDTO.getNickname())));
         return success(pageResulVO);
     }
 
+    @PostMapping("/send")
+    @Operation(summary = "发送优惠劵")
+    @PreAuthorize("@ss.hasPermission('promotion:coupon:send')")
+    public CommonResult<Boolean> sendCoupon(@Valid @RequestBody CouponSendReqVO reqVO) {
+        couponService.takeCouponByAdmin(reqVO.getTemplateId(), reqVO.getUserIds());
+        return success(true);
+    }
+
 }

+ 0 - 1
yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/controller/admin/coupon/CouponTemplateController.java

@@ -75,5 +75,4 @@ public class CouponTemplateController {
         PageResult<CouponTemplateDO> pageResult = couponTemplateService.getCouponTemplatePage(pageVO);
         return success(CouponTemplateConvert.INSTANCE.convertPage(pageResult));
     }
-
 }

+ 3 - 3
yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/controller/admin/coupon/vo/coupon/CouponBaseVO.java

@@ -26,7 +26,7 @@ public class CouponBaseVO {
     // ========== 基本信息 BEGIN ==========
     @Schema(description = "优惠劵模板编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024")
     @NotNull(message = "优惠劵模板编号不能为空")
-    private Integer templateId;
+    private Long templateId;
 
     @Schema(description = "优惠劵名", requiredMode = Schema.RequiredMode.REQUIRED, example = "春节送送送")
     @NotNull(message = "优惠劵名不能为空")
@@ -67,8 +67,8 @@ public class CouponBaseVO {
     @InEnum(PromotionProductScopeEnum.class)
     private Integer productScope;
 
-    @Schema(description = "商品 SPU 编号的数组", example = "1,3")
-    private List<Long> productSpuIds;
+    @Schema(description = "商品范围编号的数组", example = "1,3")
+    private List<Long> productScopeValues;
     // ========== 使用规则 END ==========
 
     // ========== 使用效果 BEGIN ==========

+ 24 - 0
yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/controller/admin/coupon/vo/coupon/CouponSendReqVO.java

@@ -0,0 +1,24 @@
+package cn.iocoder.yudao.module.promotion.controller.admin.coupon.vo.coupon;
+
+import io.swagger.v3.oas.annotations.media.Schema;
+import lombok.Data;
+import lombok.ToString;
+
+import javax.validation.constraints.NotEmpty;
+import javax.validation.constraints.NotNull;
+import java.util.Set;
+
+@Schema(description = "管理后台 - 优惠劵发放 Request VO")
+@Data
+@ToString(callSuper = true)
+public class CouponSendReqVO {
+
+    @Schema(description = "优惠劵模板编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024")
+    @NotNull(message = "优惠劵模板编号不能为空")
+    private Long templateId;
+
+    @Schema(description = "用户编号列表", requiredMode = Schema.RequiredMode.REQUIRED, example = "[1, 2]")
+    @NotEmpty(message = "用户编号列表不能为空")
+    private Set<Long> userIds;
+
+}

+ 5 - 5
yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/controller/admin/coupon/vo/template/CouponTemplateBaseVO.java

@@ -54,8 +54,8 @@ public class CouponTemplateBaseVO {
     @InEnum(PromotionProductScopeEnum.class)
     private Integer productScope;
 
-    @Schema(description = "商品 SPU 编号的数组", example = "1,3")
-    private List<Long> productSpuIds;
+    @Schema(description = "商品范围编号的数组", example = "[1, 3]")
+    private List<Long> productScopeValues;
 
     @Schema(description = "生效日期类型", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
     @NotNull(message = "生效日期类型不能为空")
@@ -95,11 +95,11 @@ public class CouponTemplateBaseVO {
     @Schema(description = "折扣上限", example = "100") // 单位:分,仅在 discountType 为 PERCENT 使用
     private Integer discountLimitPrice;
 
-    @AssertTrue(message = "商品 SPU 编号的数组不能为空")
+    @AssertTrue(message = "商品范围编号的数组不能为空")
     @JsonIgnore
-    public boolean isProductSpuIdsValid() {
+    public boolean isProductScopeValuesValid() {
         return Objects.equals(productScope, PromotionProductScopeEnum.ALL.getScope()) // 全部范围时,可以为空
-                || CollUtil.isNotEmpty(productSpuIds);
+                || CollUtil.isNotEmpty(productScopeValues);
     }
 
     @AssertTrue(message = "生效开始时间不能为空")

+ 7 - 0
yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/controller/admin/coupon/vo/template/CouponTemplatePageReqVO.java

@@ -1,6 +1,8 @@
 package cn.iocoder.yudao.module.promotion.controller.admin.coupon.vo.template;
 
 import cn.iocoder.yudao.framework.common.pojo.PageParam;
+import cn.iocoder.yudao.framework.common.validation.InEnum;
+import cn.iocoder.yudao.module.promotion.enums.coupon.CouponTakeTypeEnum;
 import io.swagger.v3.oas.annotations.media.Schema;
 import lombok.Data;
 import lombok.EqualsAndHashCode;
@@ -8,6 +10,7 @@ import lombok.ToString;
 import org.springframework.format.annotation.DateTimeFormat;
 
 import java.time.LocalDateTime;
+import java.util.List;
 
 import static cn.iocoder.yudao.framework.common.util.date.DateUtils.FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND;
 
@@ -30,4 +33,8 @@ public class CouponTemplatePageReqVO extends PageParam {
     @DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND)
     private LocalDateTime[] createTime;
 
+    @Schema(description = "可以领取的类型", example = "[1,2, 3]")
+    @InEnum(value = CouponTakeTypeEnum.class, message = "可以领取的类型,必须是 {value}")
+    private List<Integer> canTakeTypes;
+
 }

+ 2 - 2
yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/controller/app/coupon/vo/template/AppCouponTemplateRespVO.java

@@ -28,8 +28,8 @@ public class AppCouponTemplateRespVO {
 //    @InEnum(PromotionProductScopeEnum.class)
 //    private Integer productScope;
 //
-//    @Schema(description = "商品 SPU 编号的数组", example = "1,3")
-//    private List<Long> productSpuIds;
+//    @Schema(description = "商品范围编号的数组", example = "1,3")
+//    private List<Long> productScopeValues;
 
     @Schema(description = "生效日期类型", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
     private Integer validityType;

+ 28 - 0
yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/convert/coupon/CouponConvert.java

@@ -4,9 +4,14 @@ import cn.iocoder.yudao.framework.common.pojo.PageResult;
 import cn.iocoder.yudao.module.promotion.api.coupon.dto.CouponRespDTO;
 import cn.iocoder.yudao.module.promotion.controller.admin.coupon.vo.coupon.CouponPageItemRespVO;
 import cn.iocoder.yudao.module.promotion.dal.dataobject.coupon.CouponDO;
+import cn.iocoder.yudao.module.promotion.dal.dataobject.coupon.CouponTemplateDO;
+import cn.iocoder.yudao.module.promotion.enums.coupon.CouponStatusEnum;
+import cn.iocoder.yudao.module.promotion.enums.coupon.CouponTemplateValidityTypeEnum;
 import org.mapstruct.Mapper;
 import org.mapstruct.factory.Mappers;
 
+import java.time.LocalDateTime;
+
 /**
  * 优惠劵 Convert
  *
@@ -21,4 +26,27 @@ public interface CouponConvert {
 
     CouponRespDTO convert(CouponDO bean);
 
+    default CouponDO convert(CouponTemplateDO template, Long userId) {
+        CouponDO couponDO = new CouponDO()
+                .setTemplateId(template.getId())
+                .setName(template.getName())
+                .setTakeType(template.getTakeType())
+                .setUsePrice(template.getUsePrice())
+                .setProductScope(template.getProductScope())
+                .setProductScopeValues(template.getProductScopeValues())
+                .setDiscountType(template.getDiscountType())
+                .setDiscountPercent(template.getDiscountPercent())
+                .setDiscountPrice(template.getDiscountPrice())
+                .setDiscountLimitPrice(template.getDiscountLimitPrice())
+                .setStatus(CouponStatusEnum.UNUSED.getStatus())
+                .setUserId(userId);
+        if (CouponTemplateValidityTypeEnum.DATE.getType().equals(template.getValidityType())) {
+            couponDO.setValidStartTime(template.getValidStartTime());
+            couponDO.setValidEndTime(template.getValidEndTime());
+        } else if (CouponTemplateValidityTypeEnum.TERM.getType().equals(template.getValidityType())) {
+            couponDO.setValidStartTime(LocalDateTime.now().plusDays(template.getFixedStartTerm()));
+            couponDO.setValidEndTime(LocalDateTime.now().plusDays(template.getFixedEndTerm()));
+        }
+        return couponDO;
+    }
 }

+ 4 - 4
yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/dal/dataobject/coupon/CouponDO.java

@@ -36,7 +36,7 @@ public class CouponDO extends BaseDO {
      *
      * 关联 {@link CouponTemplateDO#getId()}
      */
-    private Integer templateId;
+    private Long templateId;
     /**
      * 优惠劵名
      *
@@ -89,12 +89,12 @@ public class CouponDO extends BaseDO {
      */
     private Integer productScope;
     /**
-     * 商品 SPU 编号的数组
+     * 商品范围编号的数组
      *
-     * 冗余 {@link CouponTemplateDO#getProductSpuIds()}
+     * 冗余 {@link CouponTemplateDO#getProductScopeValues()}
      */
     @TableField(typeHandler = LongListTypeHandler.class)
-    private List<Long> productSpuIds;
+    private List<Long> productScopeValues;
     // ========== 使用规则 END ==========
 
     // ========== 使用效果 BEGIN ==========

+ 2 - 2
yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/dal/dataobject/coupon/CouponTemplateDO.java

@@ -85,10 +85,10 @@ public class CouponTemplateDO extends BaseDO {
      */
     private Integer productScope;
     /**
-     * 商品 SPU 编号的数组
+     * 商品范围编号的数组
      */
     @TableField(typeHandler = LongListTypeHandler.class)
-    private List<Long> productSpuIds;
+    private List<Long> productScopeValues;
     /**
      * 生效日期类型
      *

+ 7 - 0
yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/dal/mysql/coupon/CouponMapper.java

@@ -55,4 +55,11 @@ public interface CouponMapper extends BaseMapperX<CouponDO> {
                 .eq(CouponDO::getStatus, status));
     }
 
+    default List<CouponDO> selectListByTemplateIdAndUserId(Long templateId, Collection<Long> userIds) {
+        return selectList(new LambdaQueryWrapperX<CouponDO>()
+                .eq(CouponDO::getTemplateId, templateId)
+                .in(CouponDO::getUserId, userIds)
+        );
+    }
+
 }

+ 18 - 0
yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/dal/mysql/coupon/CouponTemplateMapper.java

@@ -1,13 +1,19 @@
 package cn.iocoder.yudao.module.promotion.dal.mysql.coupon;
 
+import cn.hutool.core.collection.CollUtil;
+import cn.iocoder.yudao.framework.common.enums.CommonStatusEnum;
 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.coupon.vo.template.CouponTemplatePageReqVO;
 import cn.iocoder.yudao.module.promotion.dal.dataobject.coupon.CouponTemplateDO;
+import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
 import org.apache.ibatis.annotations.Mapper;
 import org.apache.ibatis.annotations.Param;
 
+import java.time.LocalDateTime;
+import java.util.function.Consumer;
+
 /**
  * 优惠劵模板 Mapper
  *
@@ -17,11 +23,23 @@ import org.apache.ibatis.annotations.Param;
 public interface CouponTemplateMapper extends BaseMapperX<CouponTemplateDO> {
 
     default PageResult<CouponTemplateDO> selectPage(CouponTemplatePageReqVO reqVO) {
+        // 构建可领取的查询条件, 好啰嗦  ( ╯-_-)╯┴—┴
+        Consumer<LambdaQueryWrapper<CouponTemplateDO>> canTakeConsumer = null;
+        if (CollUtil.isNotEmpty(reqVO.getCanTakeTypes())) {
+            canTakeConsumer = w ->
+                    w.eq(CouponTemplateDO::getStatus, CommonStatusEnum.ENABLE.getStatus()) // 1. 状态为可用的
+                            .in(CouponTemplateDO::getTakeType, reqVO.getCanTakeTypes()) // 2. 领取方式一致
+                            .and(ww -> ww.isNull(CouponTemplateDO::getValidEndTime)  // 3. 未过期
+                                    .or().gt(CouponTemplateDO::getValidEndTime, LocalDateTime.now()))
+                            .apply(" take_count < total_count "); // 4. 剩余数量大于 0
+        }
+        // 执行分页查询
         return selectPage(reqVO, new LambdaQueryWrapperX<CouponTemplateDO>()
                 .likeIfPresent(CouponTemplateDO::getName, reqVO.getName())
                 .eqIfPresent(CouponTemplateDO::getStatus, reqVO.getStatus())
                 .eqIfPresent(CouponTemplateDO::getDiscountType, reqVO.getDiscountType())
                 .betweenIfPresent(CouponTemplateDO::getCreateTime, reqVO.getCreateTime())
+                .and(canTakeConsumer != null, canTakeConsumer)
                 .orderByDesc(CouponTemplateDO::getId));
     }
 

+ 49 - 0
yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/service/coupon/CouponService.java

@@ -1,10 +1,13 @@
 package cn.iocoder.yudao.module.promotion.service.coupon;
 
+import cn.hutool.core.collection.CollUtil;
 import cn.iocoder.yudao.framework.common.pojo.PageResult;
 import cn.iocoder.yudao.module.promotion.controller.admin.coupon.vo.coupon.CouponPageReqVO;
 import cn.iocoder.yudao.module.promotion.dal.dataobject.coupon.CouponDO;
+import cn.iocoder.yudao.module.promotion.enums.coupon.CouponTakeTypeEnum;
 
 import java.util.List;
+import java.util.Set;
 
 /**
  * 优惠劵 Service 接口
@@ -51,6 +54,13 @@ public interface CouponService {
      */
     void useCoupon(Long id, Long userId, Long orderId);
 
+    /**
+     * 退还已使用的优惠券
+     *
+     * @param id 优惠券编号
+     */
+    void returnUsedCoupon(Long id);
+
     /**
      * 回收优惠劵
      *
@@ -75,4 +85,43 @@ public interface CouponService {
      */
     Long getUnusedCouponCount(Long userId);
 
+    /**
+     * 领取优惠券
+     *
+     * @param templateId 优惠券模板编号
+     * @param userIds    用户编号列表
+     * @param takeType   领取方式
+     */
+    void takeCoupon(Long templateId, Set<Long> userIds, CouponTakeTypeEnum takeType);
+
+    /**
+     * 【管理员】给用户发送优惠券
+     *
+     * @param templateId 优惠券模板编号
+     * @param userIds    用户编号列表
+     */
+    default void takeCouponByAdmin(Long templateId, Set<Long> userIds) {
+        takeCoupon(templateId, userIds, CouponTakeTypeEnum.ADMIN);
+    }
+
+    /**
+     * 【会员】领取优惠券
+     *
+     * @param templateId 优惠券模板编号
+     * @param userId     用户编号
+     */
+    default void takeCouponByUser(Long templateId, Long userId) {
+        takeCoupon(templateId, CollUtil.newHashSet(userId), CouponTakeTypeEnum.USER);
+    }
+
+    /**
+     * 【系统】给用户发送新人券
+     *
+     * @param templateId 优惠券模板编号
+     * @param userId     用户编号列表
+     */
+    default void takeCouponByRegister(Long templateId, Long userId) {
+        takeCoupon(templateId, CollUtil.newHashSet(userId), CouponTakeTypeEnum.REGISTER);
+    }
+
 }

+ 103 - 0
yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/service/coupon/CouponServiceImpl.java

@@ -1,6 +1,8 @@
 package cn.iocoder.yudao.module.promotion.service.coupon;
 
+import cn.hutool.core.collection.CollStreamUtil;
 import cn.hutool.core.collection.CollUtil;
+import cn.hutool.core.map.MapUtil;
 import cn.hutool.core.util.ObjectUtil;
 import cn.hutool.core.util.StrUtil;
 import cn.iocoder.yudao.framework.common.pojo.PageResult;
@@ -9,9 +11,13 @@ import cn.iocoder.yudao.framework.common.util.date.LocalDateTimeUtils;
 import cn.iocoder.yudao.module.member.api.user.MemberUserApi;
 import cn.iocoder.yudao.module.member.api.user.dto.MemberUserRespDTO;
 import cn.iocoder.yudao.module.promotion.controller.admin.coupon.vo.coupon.CouponPageReqVO;
+import cn.iocoder.yudao.module.promotion.convert.coupon.CouponConvert;
 import cn.iocoder.yudao.module.promotion.dal.dataobject.coupon.CouponDO;
+import cn.iocoder.yudao.module.promotion.dal.dataobject.coupon.CouponTemplateDO;
 import cn.iocoder.yudao.module.promotion.dal.mysql.coupon.CouponMapper;
 import cn.iocoder.yudao.module.promotion.enums.coupon.CouponStatusEnum;
+import cn.iocoder.yudao.module.promotion.enums.coupon.CouponTakeTypeEnum;
+import cn.iocoder.yudao.module.promotion.enums.coupon.CouponTemplateValidityTypeEnum;
 import org.springframework.stereotype.Service;
 import org.springframework.transaction.annotation.Transactional;
 import org.springframework.validation.annotation.Validated;
@@ -19,9 +25,12 @@ import org.springframework.validation.annotation.Validated;
 import javax.annotation.Resource;
 import java.time.LocalDateTime;
 import java.util.List;
+import java.util.Map;
 import java.util.Set;
+import java.util.stream.Collectors;
 
 import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception;
+import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertList;
 import static cn.iocoder.yudao.module.promotion.enums.ErrorCodeConstants.*;
 import static java.util.Arrays.asList;
 
@@ -84,6 +93,7 @@ public class CouponServiceImpl implements CouponService {
     public void useCoupon(Long id, Long userId, Long orderId) {
         // 校验优惠劵
         validCoupon(id, userId);
+
         // 更新状态
         int updateCount = couponMapper.updateByIdAndStatus(id, CouponStatusEnum.UNUSED.getStatus(),
                 new CouponDO().setStatus(CouponStatusEnum.USED.getStatus())
@@ -93,6 +103,31 @@ public class CouponServiceImpl implements CouponService {
         }
     }
 
+    @Override
+    public void returnUsedCoupon(Long id) {
+        // 校验存在
+        CouponDO coupon = couponMapper.selectById(id);
+        if (coupon == null) {
+            throw exception(COUPON_NOT_EXISTS);
+        }
+        // 校验状态
+        if (ObjectUtil.notEqual(coupon.getTemplateId(), CouponStatusEnum.USED.getStatus())) {
+            throw exception(COUPON_STATUS_NOT_USED);
+        }
+
+        // 退还
+        Integer status = LocalDateTimeUtils.beforeNow(coupon.getValidEndTime())
+                ? CouponStatusEnum.EXPIRE.getStatus() // 退还时可能已经过期了
+                : CouponStatusEnum.UNUSED.getStatus();
+        int updateCount = couponMapper.updateByIdAndStatus(id, CouponStatusEnum.UNUSED.getStatus(),
+                new CouponDO().setStatus(status));
+        if (updateCount == 0) {
+            throw exception(COUPON_STATUS_NOT_USED);
+        }
+
+        // TODO 增加优惠券变动记录?
+    }
+
     @Override
     @Transactional
     public void deleteCoupon(Long id) {
@@ -125,4 +160,72 @@ public class CouponServiceImpl implements CouponService {
         return couponMapper.selectCountByUserIdAndStatus(userId, CouponStatusEnum.UNUSED.getStatus());
     }
 
+    @Override
+    public void takeCoupon(Long templateId, Set<Long> userIds, CouponTakeTypeEnum takeType) {
+        CouponTemplateDO template = couponTemplateService.getCouponTemplate(templateId);
+        // 1. 过滤掉达到领取限制的用户
+        removeTakeLimitUser(userIds, template);
+        // 2. 校验优惠劵是否可以领取
+        validateCouponTemplateCanTake(template, userIds, takeType);
+
+        // 3. 批量保存优惠劵
+        couponMapper.insertBatch(convertList(userIds, userId -> CouponConvert.INSTANCE.convert(template, userId)));
+
+        // 3. 增加优惠劵模板的领取数量
+        couponTemplateService.updateCouponTemplateTakeCount(templateId, userIds.size());
+    }
+
+    /**
+     * 校验优惠券是否可以领取
+     *
+     * @param couponTemplate 优惠券模板
+     * @param userIds        领取人列表
+     * @param takeType       领取方式
+     */
+    private void validateCouponTemplateCanTake(CouponTemplateDO couponTemplate, Set<Long> userIds, CouponTakeTypeEnum takeType) {
+        // 如果所有用户都领取过,则抛出异常
+        if (CollUtil.isEmpty(userIds)) {
+            throw exception(COUPON_TEMPLATE_USER_ALREADY_TAKE);
+        }
+
+        // 校验模板
+        if (couponTemplate == null) {
+            throw exception(COUPON_TEMPLATE_NOT_EXISTS);
+        }
+        // 校验剩余数量
+        if (couponTemplate.getTakeCount() + userIds.size() > couponTemplate.getTotalCount()) {
+            throw exception(COUPON_TEMPLATE_NOT_ENOUGH);
+        }
+        // 校验"固定日期"的有效期类型是否过期
+        if (CouponTemplateValidityTypeEnum.DATE.getType().equals(couponTemplate.getValidityType())) {
+            if (LocalDateTimeUtils.beforeNow(couponTemplate.getValidEndTime())) {
+                throw exception(COUPON_TEMPLATE_EXPIRED);
+            }
+        }
+        // 校验领取方式
+        if (ObjectUtil.notEqual(couponTemplate.getTakeType(), takeType.getValue())) {
+            throw exception(COUPON_TEMPLATE_CANNOT_TAKE);
+        }
+    }
+
+    /**
+     * 过滤掉达到领取上线的用户
+     *
+     * @param userIds 用户编号数组
+     * @param couponTemplate 优惠劵模版
+     */
+    private void removeTakeLimitUser(Set<Long> userIds, CouponTemplateDO couponTemplate) {
+        if (couponTemplate.getTakeLimitCount() <= 0) {
+            return;
+        }
+        // 查询已领过券的用户
+        List<CouponDO> alreadyTakeCoupons = couponMapper.selectListByTemplateIdAndUserId(couponTemplate.getId(), userIds);
+        if (CollUtil.isEmpty(alreadyTakeCoupons)) {
+            return;
+        }
+        // 移除达到领取限制的用户
+        Map<Long, Integer> userTakeCountMap = CollStreamUtil.groupBy(alreadyTakeCoupons, CouponDO::getUserId, Collectors.summingInt(c -> 1));
+        userIds.removeIf(userId -> MapUtil.getInt(userTakeCountMap, userId, 0) >= couponTemplate.getTakeLimitCount());
+    }
+
 }

+ 2 - 2
yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/service/coupon/CouponTemplateService.java

@@ -33,7 +33,7 @@ public interface CouponTemplateService {
     /**
      * 更新优惠劵模板的状态
      *
-     * @param id 编号
+     * @param id     编号
      * @param status 状态
      */
     void updateCouponTemplateStatus(Long id, Integer status);
@@ -64,7 +64,7 @@ public interface CouponTemplateService {
     /**
      * 更新优惠劵模板的领取数量
      *
-     * @param id 优惠劵模板编号
+     * @param id        优惠劵模板编号
      * @param incrCount 增加数量
      */
     void updateCouponTemplateTakeCount(Long id, int incrCount);

+ 2 - 1
yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/service/coupon/CouponTemplateServiceImpl.java

@@ -14,7 +14,8 @@ import org.springframework.validation.annotation.Validated;
 import javax.annotation.Resource;
 
 import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception;
-import static cn.iocoder.yudao.module.promotion.enums.ErrorCodeConstants.*;
+import static cn.iocoder.yudao.module.promotion.enums.ErrorCodeConstants.COUPON_TEMPLATE_NOT_EXISTS;
+import static cn.iocoder.yudao.module.promotion.enums.ErrorCodeConstants.COUPON_TEMPLATE_TOTAL_COUNT_TOO_SMALL;
 
 /**
  * 优惠劵模板 Service 实现类

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

@@ -33,6 +33,7 @@ public interface ErrorCodeConstants {
     ErrorCode ORDER_DELIVERY_FAIL_COMBINATION_RECORD_STATUS_NOT_SUCCESS = new ErrorCode(1011000022, "交易订单发货失败,拼团未成功");
     ErrorCode ORDER_DELIVERY_FAIL_BARGAIN_RECORD_STATUS_NOT_SUCCESS = new ErrorCode(1011000023, "交易订单发货失败,砍价未成功");
     ErrorCode ORDER_DELIVERY_FAIL_DELIVERY_TYPE_NOT_EXPRESS = new ErrorCode(1011000024, "交易订单发货失败,发货类型不是快递");
+    ErrorCode ORDER_CANCEL_FAIL_STATUS_NOT_UNPAID = new ErrorCode(1011000025, "交易订单取消失败,订单不是【待支付】状态");
 
     // ==========  After Sale 模块 1011000100 ==========
     ErrorCode AFTER_SALE_NOT_FOUND = new ErrorCode(1011000100, "售后单不存在");

+ 20 - 0
yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/controller/admin/aftersale/TradeAfterSaleController.java

@@ -27,6 +27,8 @@ import org.springframework.web.bind.annotation.*;
 import javax.annotation.Resource;
 import javax.annotation.security.PermitAll;
 import javax.validation.Valid;
+import java.time.LocalDateTime;
+import java.util.ArrayList;
 import java.util.List;
 import java.util.Map;
 
@@ -82,6 +84,24 @@ public class TradeAfterSaleController {
         MemberUserRespDTO user = memberUserApi.getUser(afterSale.getUserId());
         // 获取售后日志
         List<TradeAfterSaleLogRespDTO> logs = afterSaleLogService.getLog(afterSale.getId());
+        // TODO 方便测试看效果,review 后移除
+        if (logs == null) {
+            logs = new ArrayList<>();
+        }
+        for (int i = 1; i <= 6; i++) {
+            TradeAfterSaleLogRespDTO respVO = new TradeAfterSaleLogRespDTO();
+            respVO.setId((long) i);
+            respVO.setUserId((long) i);
+            respVO.setUserType(1);
+            respVO.setAfterSaleId(id);
+            respVO.setOrderId((long) i);
+            respVO.setOrderItemId((long) i);
+            respVO.setBeforeStatus((i - 1) * 10);
+            respVO.setAfterStatus(i * 10);
+            respVO.setContent("66+6");
+            respVO.setCreateTime(LocalDateTime.now());
+            logs.add(respVO);
+        }
         return success(TradeAfterSaleConvert.INSTANCE.convert(afterSale, order, orderItems, user, logs));
     }
 

+ 2 - 10
yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/controller/admin/order/TradeOrderController.java

@@ -25,7 +25,6 @@ import java.util.Map;
 
 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;
 
 @Tag(name = "管理后台 - 交易订单")
 @RestController
@@ -70,7 +69,7 @@ public class TradeOrderController {
         TradeOrderDO order = tradeOrderQueryService.getOrder(id);
         // 查询订单项
         List<TradeOrderItemDO> orderItems = tradeOrderQueryService.getOrderItemListByOrderId(id);
-
+        // orderLog
         // 拼接数据
         MemberUserRespDTO user = memberUserApi.getUser(order.getUserId());
         return success(TradeOrderConvert.INSTANCE.convert(order, orderItems, user));
@@ -82,7 +81,7 @@ public class TradeOrderController {
     @PreAuthorize("@ss.hasPermission('trade:order:query')")
     public CommonResult<List<?>> getOrderExpressTrackList(@RequestParam("id") Long id) {
         return success(TradeOrderConvert.INSTANCE.convertList02(
-                tradeOrderQueryService.getExpressTrackList(id, getLoginUserId())));
+                tradeOrderQueryService.getExpressTrackList(id)));
     }
 
     @PutMapping("/delivery")
@@ -117,11 +116,4 @@ public class TradeOrderController {
         return success(true);
     }
 
-    // TODO @puhui999 订单物流详情
-    // TODO @puhui999 【前台】订单取消
-    // TODO @puhui999 【后台】订单取消
-    // TODO @puhui999 【前台】订单核销
-    // TODO @puhui999 【前台】订单删除
-    // TODO @puhui999 【后台】订单统计
-
 }

+ 22 - 0
yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/controller/admin/order/vo/TradeOrderDetailRespVO.java

@@ -5,6 +5,7 @@ import cn.iocoder.yudao.module.trade.controller.admin.base.product.property.Prod
 import io.swagger.v3.oas.annotations.media.Schema;
 import lombok.Data;
 
+import java.time.LocalDateTime;
 import java.util.List;
 
 @Schema(description = "管理后台 - 交易订单的详情 Response VO")
@@ -24,6 +25,27 @@ public class TradeOrderDetailRespVO extends TradeOrderBaseVO {
      */
     private MemberUserRespVO user;
 
+    /**
+     * TODO 订单操作日志, 先模拟一波;返回 logs,简洁,然后复数哈
+     */
+    private List<OrderLog> orderLog;
+
+    // TODO @puhui999:swagger 注解
+    @Data
+    public static class OrderLog {
+
+        /**
+         * 内容
+         */
+        private String content;
+
+        /**
+         * 创建时间
+         */
+        private LocalDateTime createTime;
+
+    }
+
     @Schema(description = "管理后台 - 交易订单的详情的订单项目")
     @Data
     public static class Item extends TradeOrderItemBaseVO {

+ 1 - 1
yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/controller/admin/order/vo/TradeOrderPageReqVO.java

@@ -61,5 +61,5 @@ public class TradeOrderPageReqVO extends PageParam {
     @Schema(description = "订单来源", example = "10")
     @InEnum(value = TerminalEnum.class, message = "订单来源 {value}")
     private Integer terminal;
-
+//    TODO 添加配送方式筛选
 }

+ 2 - 0
yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/controller/app/delivery/AppDeliverPickUpStoreController.java

@@ -24,6 +24,7 @@ import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success;
 @Validated
 public class AppDeliverPickUpStoreController {
 
+    // TODO 待实现[门店自提]:如果 latitude、longitude 非空,计算经纬度,并排序。计算的库,可以使用 hutool 的 DistanceUtil 计算。
     @GetMapping("/list")
     @Operation(summary = "获得自提门店列表")
     public CommonResult<List<AppDeliveryPickUpStoreRespVO>> getDeliveryPickUpStoreList(
@@ -50,6 +51,7 @@ public class AppDeliverPickUpStoreController {
         return success(list);
     }
 
+    // TODO 待实现[门店自提]:
     @GetMapping("/get")
     @Operation(summary = "获得自提门店")
     @Parameter(name = "id", description = "门店编号")

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

@@ -49,9 +49,6 @@ public class AppTradeOrderController {
     @Resource
     private DeliveryExpressService deliveryExpressService;
 
-    @Resource
-    private ProductPropertyValueApi productPropertyValueApi;
-
     @Resource
     private TradeOrderProperties tradeOrderProperties;
 
@@ -78,6 +75,7 @@ public class AppTradeOrderController {
         return success(true);
     }
 
+    // TODO @芋艿:如果拼团活动、秒杀活动、砍价活动时,是不是要额外在返回活动之类的信息;
     @GetMapping("/get-detail")
     @Operation(summary = "获得交易订单")
     @Parameter(name = "id", description = "交易订单编号")
@@ -93,6 +91,7 @@ public class AppTradeOrderController {
         // 查询物流公司
         DeliveryExpressDO express = order.getLogisticsId() != null && order.getLogisticsId() > 0 ?
                 deliveryExpressService.getDeliveryExpress(order.getLogisticsId()) : null;
+        // TODO @puhui999:如果门店自提,信息的拼接;
         // 最终组合
         return success(TradeOrderConvert.INSTANCE.convert02(order, orderItems, tradeOrderProperties, express));
     }
@@ -141,7 +140,7 @@ public class AppTradeOrderController {
     @PutMapping("/receive")
     @Operation(summary = "确认交易订单收货")
     @Parameter(name = "id", description = "交易订单编号")
-    public CommonResult<Boolean> takeOrder(@RequestParam("id") Long id) {
+    public CommonResult<Boolean> receiveOrder(@RequestParam("id") Long id) {
         tradeOrderUpdateService.receiveOrder(getLoginUserId(), id);
         return success(true);
     }
@@ -150,7 +149,7 @@ public class AppTradeOrderController {
     @Operation(summary = "取消交易订单")
     @Parameter(name = "id", description = "交易订单编号")
     public CommonResult<Boolean> cancelOrder(@RequestParam("id") Long id) {
-        // TODO @芋艿:未实现,mock 用
+        tradeOrderUpdateService.cancelOrder(getLoginUserId(), id);
         return success(true);
     }
 

+ 4 - 2
yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/convert/aftersale/TradeAfterSaleConvert.java

@@ -72,16 +72,18 @@ public interface TradeAfterSaleConvert {
     
     default TradeAfterSaleDetailRespVO convert(TradeAfterSaleDO afterSale, TradeOrderDO order, List<TradeOrderItemDO> orderItems,
                                                MemberUserRespDTO user, List<TradeAfterSaleLogRespDTO> logs) {
-        TradeAfterSaleDetailRespVO respVO = convert(afterSale, orderItems, convertList1(logs));
+        TradeAfterSaleDetailRespVO respVO = convert(afterSale, orderItems);
         // 处理用户信息
         respVO.setUser(convert(user));
         // 处理订单信息
         respVO.setOrder(convert(order));
+        // 处理售后日志
+        respVO.setAfterSaleLog(convertList1(logs));
         return respVO;
     }
     List<TradeAfterSaleLogRespVO> convertList1(List<TradeAfterSaleLogRespDTO> list);
     @Mapping(target = "id", source = "afterSale.id")
-    TradeAfterSaleDetailRespVO convert(TradeAfterSaleDO afterSale, List<TradeOrderItemDO> orderItems, List<TradeAfterSaleLogRespVO> logs);
+    TradeAfterSaleDetailRespVO convert(TradeAfterSaleDO afterSale, List<TradeOrderItemDO> orderItems);
     TradeOrderBaseVO convert(TradeOrderDO order);
 
 }

+ 20 - 3
yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/convert/order/TradeOrderConvert.java

@@ -1,6 +1,5 @@
 package cn.iocoder.yudao.module.trade.convert.order;
 
-import cn.hutool.core.collection.CollUtil;
 import cn.hutool.core.util.StrUtil;
 import cn.iocoder.yudao.framework.common.pojo.PageResult;
 import cn.iocoder.yudao.framework.common.util.collection.CollectionUtils;
@@ -37,8 +36,10 @@ import org.mapstruct.Mapping;
 import org.mapstruct.Mappings;
 import org.mapstruct.factory.Mappers;
 
-import java.util.*;
-import java.util.stream.Collectors;
+import java.time.LocalDateTime;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Map;
 
 import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertMap;
 import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertMultiMap;
@@ -86,7 +87,14 @@ public interface TradeOrderConvert {
     default ProductSkuUpdateStockReqDTO convert(List<TradeOrderItemDO> list) {
         return new ProductSkuUpdateStockReqDTO(TradeOrderConvert.INSTANCE.convertList(list));
     }
+
+    default ProductSkuUpdateStockReqDTO convertNegative(List<TradeOrderItemDO> list) {
+        List<ProductSkuUpdateStockReqDTO.Item> items = TradeOrderConvert.INSTANCE.convertList(list);
+        items.forEach(item -> item.setIncrCount(-item.getIncrCount()));
+        return new ProductSkuUpdateStockReqDTO(items);
+    }
     List<ProductSkuUpdateStockReqDTO.Item> convertList(List<TradeOrderItemDO> list);
+
     @Mappings({
             @Mapping(source = "skuId", target = "id"),
             @Mapping(source = "count", target = "incrCount"),
@@ -137,6 +145,15 @@ public interface TradeOrderConvert {
         orderVO.setReceiverAreaName(AreaUtils.format(order.getReceiverAreaId()));
         // 处理用户信息
         orderVO.setUser(convert(user));
+        // TODO puhui999:模拟订单操作日志
+        ArrayList<TradeOrderDetailRespVO.OrderLog> orderLogs = new ArrayList<>();
+        for (int i = 0; i < 6; i++) {
+            TradeOrderDetailRespVO.OrderLog orderLog = new TradeOrderDetailRespVO.OrderLog();
+            orderLog.setContent("订单操作" + i);
+            orderLog.setCreateTime(LocalDateTime.now());
+            orderLogs.add(orderLog);
+        }
+        orderVO.setOrderLog(orderLogs);
         return orderVO;
     }
 

+ 2 - 0
yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/dal/dataobject/order/TradeOrderItemDO.java

@@ -143,6 +143,8 @@ public class TradeOrderItemDO extends BaseDO {
      * 对应 taobao 的 trade.point_fee 字段
      */
     private Integer pointPrice;
+    // TODO @芋艿:如果商品 vip 折扣时,到底是新增一个 vipPrice 记录优惠记录,还是 vipDiscountPrice,记录 vip 的优惠;还是直接使用 vipPrice;
+    // 目前 crmeb 的选择,单独一个 vipPrice 记录优惠价格;感觉不一定合理,可以在看看有赞的;
 
     // ========== 售后基本信息 ==========
 

+ 2 - 0
yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/service/aftersale/TradeAfterSaleServiceImpl.java

@@ -98,6 +98,8 @@ public class TradeAfterSaleServiceImpl implements TradeAfterSaleService, AfterSa
         return afterSale;
     }
 
+    // TODO 芋艿:拼团失败,要不要发起售后的方式退款?还是走取消逻辑?
+
     @Override
     @Transactional(rollbackFor = Exception.class)
     public Long createAfterSale(Long userId, AppTradeAfterSaleCreateReqVO createReqVO) {

+ 9 - 1
yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/service/order/TradeOrderQueryService.java

@@ -66,7 +66,7 @@ public interface TradeOrderQueryService {
     Long getOrderCount(Long userId, Integer status, Boolean commonStatus);
 
     /**
-     * 获得订单的物流轨迹
+     * 【前台】获得订单的物流轨迹
      *
      * @param id 订单编号
      * @param userId 用户编号
@@ -74,6 +74,14 @@ public interface TradeOrderQueryService {
      */
     List<ExpressTrackRespDTO> getExpressTrackList(Long id, Long userId);
 
+    /**
+     * 【后台】获得订单的物流轨迹
+     *
+     * @param id 订单编号
+     * @return 物流轨迹数组
+     */
+    List<ExpressTrackRespDTO> getExpressTrackList(Long id);
+
     // =================== Order Item ===================
 
     /**

+ 21 - 0
yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/service/order/TradeOrderQueryServiceImpl.java

@@ -106,6 +106,27 @@ public class TradeOrderQueryServiceImpl implements TradeOrderQueryService {
             throw exception(ORDER_NOT_FOUND);
         }
 
+        return getExpressTrackList(order);
+    }
+
+    @Override
+    public List<ExpressTrackRespDTO> getExpressTrackList(Long id) {
+        // 查询订单
+        TradeOrderDO order = tradeOrderMapper.selectById(id);
+        if (order == null) {
+            throw exception(ORDER_NOT_FOUND);
+        }
+
+        return getExpressTrackList(order);
+    }
+
+    /**
+     * 获得订单的物流轨迹
+     *
+     * @param order 订单
+     * @return 物流轨迹
+     */
+    private List<ExpressTrackRespDTO> getExpressTrackList(TradeOrderDO order) {
         // 查询物流公司
         if (order.getLogisticsId() == null) {
             return Collections.emptyList();

+ 7 - 0
yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/service/order/TradeOrderUpdateService.java

@@ -117,4 +117,11 @@ public interface TradeOrderUpdateService {
      */
     Long createOrderItemComment(Long userId, AppTradeOrderItemCommentCreateReqVO createReqVO);
 
+    /**
+     * 【会员】取消订单
+     *
+     * @param userId 用户ID
+     * @param id     订单编号
+     */
+    void cancelOrder(Long userId, Long id);
 }

+ 70 - 19
yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/service/order/TradeOrderUpdateServiceImpl.java

@@ -96,6 +96,8 @@ public class TradeOrderUpdateServiceImpl implements TradeOrderUpdateService {
     private TradePriceService tradePriceService;
     @Resource
     private DeliveryExpressService deliveryExpressService;
+    @Resource
+    private TradeMessageService tradeMessageService;
 
     @Resource
     private ProductSkuApi productSkuApi;
@@ -105,26 +107,21 @@ public class TradeOrderUpdateServiceImpl implements TradeOrderUpdateService {
     private AddressApi addressApi;
     @Resource
     private CouponApi couponApi;
-
+    @Resource
+    private CombinationRecordApi combinationRecordApi;
+    @Resource
+    private BargainRecordApi bargainRecordApi;
     @Resource
     private MemberUserApi memberUserApi;
     @Resource
     private MemberLevelApi memberLevelApi;
     @Resource
     private MemberPointApi memberPointApi;
-
     @Resource
     private ProductCommentApi productCommentApi;
-    @Resource
-    private TradeMessageService tradeMessageService;
-    @Resource
-    private TradeOrderProperties tradeOrderProperties;
-
-    @Resource
-    private CombinationRecordApi combinationRecordApi;
 
     @Resource
-    private BargainRecordApi bargainRecordApi;
+    private TradeOrderProperties tradeOrderProperties;
 
     // =================== Order ===================
 
@@ -179,15 +176,17 @@ public class TradeOrderUpdateServiceImpl implements TradeOrderUpdateService {
     @Override
     @Transactional(rollbackFor = Exception.class)
     public TradeOrderDO createOrder(Long userId, String userIp, AppTradeOrderCreateReqVO createReqVO) {
-        // 2. 价格计算
+        // 1. 价格计算
         TradePriceCalculateRespBO calculateRespBO = calculatePrice(userId, createReqVO);
-        // 3.1 插入 TradeOrderDO 订单
+
+        // 2.1 插入 TradeOrderDO 订单
         TradeOrderDO order = createTradeOrder(userId, userIp, createReqVO, calculateRespBO);
-        // 3.2 插入 TradeOrderItemDO 订单项
+        // 2.2 插入 TradeOrderItemDO 订单项
         List<TradeOrderItemDO> orderItems = createTradeOrderItems(order, calculateRespBO);
-        // 订单创建完后的逻辑
+
+        // 3. 订单创建完后的逻辑
         afterCreateTradeOrder(userId, createReqVO, order, orderItems, calculateRespBO);
-        // 3.3 校验订单类型
+        // 3.1 拼团的特殊逻辑
         // TODO @puhui999:这个逻辑,先抽个小方法;未来要通过设计模式,把这些拼团之类的逻辑,抽象出去
         // 拼团
         if (Objects.equals(TradeOrderTypeEnum.COMBINATION.getType(), order.getType())) {
@@ -205,15 +204,19 @@ public class TradeOrderUpdateServiceImpl implements TradeOrderUpdateService {
 
             combinationRecordApi.createCombinationRecord(TradeOrderConvert.INSTANCE.convert(order, orderItemDO, createReqVO, user));
         }
+        // 3.2 秒杀的特殊逻辑
         // TODO 秒杀扣减库存是下单就扣除还是等待订单支付成功再扣除
         if (Objects.equals(TradeOrderTypeEnum.SECKILL.getType(), order.getType())) {
 
         }
+        // 3.3 砍价的特殊逻辑
 
         // TODO @LeeYan9: 是可以思考下, 订单的营销优惠记录, 应该记录在哪里, 微信讨论起来!
         return order;
     }
 
+    // TODO @puhui999:订单超时,自动取消;
+
     /**
      * 校验收件地址是否存在
      *
@@ -239,7 +242,7 @@ public class TradeOrderUpdateServiceImpl implements TradeOrderUpdateService {
         }
         TradeOrderDO order = TradeOrderConvert.INSTANCE.convert(userId, clientIp, createReqVO, calculateRespBO, address);
         order.setType(validateActivity(createReqVO));
-        order.setNo(IdUtil.getSnowflakeNextId() + ""); // TODO @LeeYan9: 思考下, 怎么生成好点哈; 这个是会展示给用户的;
+        order.setNo(IdUtil.getSnowflakeNextId() + ""); // TODO @puhui999: 参考支付订单,的 no 生成哈;
         order.setStatus(TradeOrderStatusEnum.UNPAID.getStatus());
         order.setRefundStatus(TradeOrderRefundStatusEnum.NONE.getStatus());
         order.setProductCount(getSumValue(calculateRespBO.getItems(), TradePriceCalculateRespBO.OrderItem::getCount, Integer::sum));
@@ -251,6 +254,7 @@ public class TradeOrderUpdateServiceImpl implements TradeOrderUpdateService {
         // 退款信息
         order.setRefundStatus(TradeOrderRefundStatusEnum.NONE.getStatus()).setRefundPrice(0);
         tradeOrderMapper.insert(order);
+        // TODO @puhui999:如果是门店订单,则需要生成核销码;
         return order;
     }
 
@@ -291,7 +295,11 @@ public class TradeOrderUpdateServiceImpl implements TradeOrderUpdateService {
                                        TradeOrderDO tradeOrderDO, List<TradeOrderItemDO> orderItems,
                                        TradePriceCalculateRespBO calculateRespBO) {
         // 下单时扣减商品库存
-        productSkuApi.updateSkuStock(TradeOrderConvert.INSTANCE.convert(orderItems));
+        // TODO @puhui999:扣库存,需要前置;
+        // 1)如果是秒杀商品:额外扣减秒杀的库存;
+        // 2)如果是拼团活动:额外扣减拼团的库存;
+        // 3)如果是砍价活动:额外扣减砍价的库存;
+        productSkuApi.updateSkuStock(TradeOrderConvert.INSTANCE.convertNegative(orderItems));
 
         // 删除购物车商品
         Set<Long> cartIds = convertSet(createReqVO.getItems(), AppTradeOrderSettlementReqVO.Item::getCartId);
@@ -299,9 +307,10 @@ public class TradeOrderUpdateServiceImpl implements TradeOrderUpdateService {
             cartService.deleteCart(userId, cartIds);
         }
 
-        // 扣减积分 TODO 芋艿:待实现
+        // 扣减积分 TODO 芋艿:待实现,需要前置;
+        // 这个是不是应该放到支付成功之后?如果支付后的话,可能积分可以重复使用哈。资源类,都要预扣
 
-        // 有使用优惠券时更新
+        // 有使用优惠券时更新 TODO 芋艿:需要前置;
         if (createReqVO.getCouponId() != null) {
             couponApi.useCoupon(new CouponUseReqDTO().setId(createReqVO.getCouponId()).setUserId(userId)
                     .setOrderId(tradeOrderDO.getId()));
@@ -510,6 +519,10 @@ public class TradeOrderUpdateServiceImpl implements TradeOrderUpdateService {
         // TODO 芋艿:lili 发送订单变化的消息
 
         // TODO 芋艿:lili 发送商品被购买完成的数据
+
+        // TODO 芋艿:销售佣金的记录;
+
+        // TODO 芋艿:获得积分;
     }
 
     @Override
@@ -529,11 +542,13 @@ public class TradeOrderUpdateServiceImpl implements TradeOrderUpdateService {
         if (order.getPayStatus()) {
             throw exception(ORDER_UPDATE_PRICE_FAIL_PAID);
         }
+        // TODO @puhui999:如果改价,需要校验下是否真的变化;
 
         // 更新
         // TODO @puhui999:TradeOrderItemDO 需要做 adjustPrice 的分摊;另外,支付订单那的价格,需要 update 下;
         TradeOrderDO update = TradeOrderConvert.INSTANCE.convert(reqVO);
         update.setPayPrice(update.getPayPrice() + update.getAdjustPrice());
+        // TODO @芋艿:改价时,赠送的积分,要不要做改动???
         tradeOrderMapper.updateById(update);
     }
 
@@ -651,10 +666,46 @@ public class TradeOrderUpdateServiceImpl implements TradeOrderUpdateService {
         List<TradeOrderItemDO> orderItems = tradeOrderItemMapper.selectListByOrderId(order.getId());
         if (!anyMatch(orderItems, item -> Objects.equals(item.getCommentStatus(), Boolean.FALSE))) {
             tradeOrderMapper.updateById(new TradeOrderDO().setId(order.getId()).setCommentStatus(Boolean.TRUE));
+            // TODO 待实现:已完成评价,要不要写一条订单日志?目前 crmeb 会写,有赞可以研究下
         }
         return comment;
     }
 
+    @Override
+    public void cancelOrder(Long userId, Long id) {
+        // 校验存在
+        TradeOrderDO order = tradeOrderMapper.selectOrderByIdAndUserId(id, userId);
+        if (order == null) {
+            throw exception(ORDER_NOT_FOUND);
+        }
+        // 校验状态
+        if (ObjectUtil.notEqual(order.getStatus(), TradeOrderStatusEnum.UNPAID.getStatus())) {
+            throw exception(ORDER_CANCEL_FAIL_STATUS_NOT_UNPAID);
+        }
+
+        // 1.更新 TradeOrderDO 状态为已取消
+        int updateCount = tradeOrderMapper.updateByIdAndStatus(id, order.getStatus(),
+                new TradeOrderDO().setStatus(TradeOrderStatusEnum.CANCELED.getStatus())
+                        .setCancelTime(LocalDateTime.now())
+                        .setCancelType(TradeOrderCancelTypeEnum.MEMBER_CANCEL.getType()));
+        if (updateCount == 0) {
+            throw exception(ORDER_CANCEL_FAIL_STATUS_NOT_UNPAID);
+        }
+
+        // 2.回滚库存
+        List<TradeOrderItemDO> orderItems = tradeOrderItemMapper.selectListByOrderId(id);
+        productSkuApi.updateSkuStock(TradeOrderConvert.INSTANCE.convert(orderItems));
+
+        // 3.回滚优惠券
+        couponApi.returnUsedCoupon(order.getCouponId());
+
+        // 4.回滚积分:积分是支付成功后才增加的吧? 回复:每个项目不同,目前看下来,确认收货貌似更合适,我再看看其它项目的业务选择;
+
+        // TODO 芋艿:OrderLog
+
+        // TODO 芋艿:lili 发送订单变化的消息
+    }
+
     /**
      * 判断指定订单的所有订单项,是不是都售后成功
      *

+ 1 - 0
yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/service/price/TradePriceServiceImpl.java

@@ -41,6 +41,7 @@ public class TradePriceServiceImpl implements TradePriceService {
     @Resource
     private List<TradePriceCalculator> priceCalculators;
 
+    // TODO @疯狂:需要搞个 TradePriceCalculator,计算赠送积分;
     @Override
     public TradePriceCalculateRespBO calculatePrice(TradePriceCalculateReqBO calculateReqBO) {
         // 1.1 获得商品 SKU 数组

+ 4 - 1
yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/service/price/calculator/TradeCouponPriceCalculator.java

@@ -102,7 +102,10 @@ public class TradeCouponPriceCalculator implements TradePriceCalculator {
         Predicate<TradePriceCalculateRespBO.OrderItem> matchPredicate = TradePriceCalculateRespBO.OrderItem::getSelected;
         if (PromotionProductScopeEnum.SPU.getScope().equals(coupon.getProductScope())) {
             matchPredicate = matchPredicate // 额外加如下条件
-                    .and(orderItem -> coupon.getProductSpuIds().contains(orderItem.getSpuId()));
+                    .and(orderItem -> coupon.getProductScopeValues().contains(orderItem.getSpuId()));
+        } else if (PromotionProductScopeEnum.CATEGORY.getScope().equals(coupon.getProductScope())) {
+            matchPredicate = matchPredicate // 额外加如下条件
+                    .and(orderItem -> coupon.getProductScopeValues().contains(orderItem.getCategoryId()));
         }
         return filterList(result.getItems(), matchPredicate);
     }

+ 1 - 0
yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/service/price/calculator/TradeDeliveryPriceCalculator.java

@@ -42,6 +42,7 @@ public class TradeDeliveryPriceCalculator implements TradePriceCalculator {
 
     @Override
     public void calculate(TradePriceCalculateReqBO param, TradePriceCalculateRespBO result) {
+        // TODO @芋艿:如果门店自提,需要校验是否开启;
         // 1.1 判断配送方式
         if (param.getDeliveryType() == null || DeliveryTypeEnum.PICK_UP.getMode().equals(param.getDeliveryType())) {
             return;

+ 1 - 1
yudao-module-mall/yudao-module-trade-biz/src/test/java/cn/iocoder/yudao/module/trade/service/price/calculator/TradeCouponPriceCalculatorTest.java

@@ -65,7 +65,7 @@ public class TradeCouponPriceCalculatorTest extends BaseMockitoUnitTest {
 
         // mock 方法(优惠劵 Coupon 信息)
         CouponRespDTO coupon = randomPojo(CouponRespDTO.class, o -> o.setId(1024L).setName("程序员节")
-                .setProductScope(PromotionProductScopeEnum.SPU.getScope()).setProductSpuIds(asList(1L, 2L))
+                .setProductScope(PromotionProductScopeEnum.SPU.getScope()).setProductScopeValues(asList(1L, 2L))
                 .setUsePrice(350).setDiscountType(PromotionDiscountTypeEnum.PERCENT.getType())
                 .setDiscountPercent(50).setDiscountLimitPrice(70));
         when(couponApi.validateCoupon(eq(new CouponValidReqDTO().setId(1024L).setUserId(233L)))).thenReturn(coupon);