diff --git a/pom.xml b/pom.xml
index 86dfebcc3..4634d345e 100644
--- a/pom.xml
+++ b/pom.xml
@@ -15,12 +15,12 @@
yudao-module-system
yudao-module-infra
-
+ yudao-module-member
-
-
+ yudao-module-pay
+ yudao-module-mall
diff --git a/yudao-framework/yudao-common/src/main/java/cn/iocoder/yudao/framework/common/util/http/HttpUtils.java b/yudao-framework/yudao-common/src/main/java/cn/iocoder/yudao/framework/common/util/http/HttpUtils.java
index 9da4f87b1..9e75113d0 100644
--- a/yudao-framework/yudao-common/src/main/java/cn/iocoder/yudao/framework/common/util/http/HttpUtils.java
+++ b/yudao-framework/yudao-common/src/main/java/cn/iocoder/yudao/framework/common/util/http/HttpUtils.java
@@ -111,7 +111,7 @@ public class HttpUtils {
authorization = Base64.decodeStr(authorization);
clientId = StrUtil.subBefore(authorization, ":", false);
clientSecret = StrUtil.subAfter(authorization, ":", false);
- // 再从 Param 中获取
+ // 再从 Param 中获取
} else {
clientId = request.getParameter("client_id");
clientSecret = request.getParameter("client_secret");
@@ -143,4 +143,21 @@ public class HttpUtils {
}
}
+ /**
+ * HTTP get 请求,基于 {@link cn.hutool.http.HttpUtil} 实现
+ *
+ * 为什么要封装该方法,因为 HttpUtil 默认封装的方法,没有允许传递 headers 参数
+ *
+ * @param url URL
+ * @param headers 请求头
+ * @return 请求结果
+ */
+ public static String get(String url, Map headers) {
+ try (HttpResponse response = HttpRequest.get(url)
+ .addHeaders(headers)
+ .execute()) {
+ return response.body();
+ }
+ }
+
}
diff --git a/yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/framework/file/core/client/s3/S3FileClient.java b/yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/framework/file/core/client/s3/S3FileClient.java
index 43ff2733b..29f6fc34f 100644
--- a/yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/framework/file/core/client/s3/S3FileClient.java
+++ b/yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/framework/file/core/client/s3/S3FileClient.java
@@ -91,7 +91,7 @@ public class S3FileClient extends AbstractFileClient {
* 开启 VirtualStyle 模式
*/
private void enableVirtualStyleEndpoint() {
- if (StrUtil.containsAll(config.getEndpoint(),
+ if (StrUtil.containsAny(config.getEndpoint(),
S3FileClientConfig.ENDPOINT_TENCENT, // 腾讯云 https://cloud.tencent.com/document/product/436/41284
S3FileClientConfig.ENDPOINT_VOLCES)) { // 火山云 https://www.volcengine.com/docs/6349/1288493
client.enableVirtualStyleEndpoint();
diff --git a/yudao-module-infra/yudao-module-infra-biz/src/main/resources/codegen/java/service/serviceImpl.vm b/yudao-module-infra/yudao-module-infra-biz/src/main/resources/codegen/java/service/serviceImpl.vm
index a8184e4d7..80bc71b02 100644
--- a/yudao-module-infra/yudao-module-infra-biz/src/main/resources/codegen/java/service/serviceImpl.vm
+++ b/yudao-module-infra/yudao-module-infra-biz/src/main/resources/codegen/java/service/serviceImpl.vm
@@ -286,6 +286,7 @@ public class ${table.className}ServiceImpl implements ${table.className}Service
// 校验存在
validate${subSimpleClassName}Exists(${subClassNameVar}.getId());
// 更新
+ ${subClassNameVar}.setUpdater(null).setUpdateTime(null); // 解决更新情况下:updateTime 不更新
${subClassNameVars.get($index)}Mapper.updateById(${subClassNameVar});
}
diff --git a/yudao-module-infra/yudao-module-infra-biz/src/main/resources/codegen/vue3/views/components/form_sub_erp.vue.vm b/yudao-module-infra/yudao-module-infra-biz/src/main/resources/codegen/vue3/views/components/form_sub_erp.vue.vm
index 3996a9caa..81cd9775e 100644
--- a/yudao-module-infra/yudao-module-infra-biz/src/main/resources/codegen/vue3/views/components/form_sub_erp.vue.vm
+++ b/yudao-module-infra/yudao-module-infra-biz/src/main/resources/codegen/vue3/views/components/form_sub_erp.vue.vm
@@ -64,12 +64,11 @@
- {{ dict.label }}
-
+ :label="dict.label"
+ :value="dict.value"
+ />
#else##没数据字典
- 请选择字典生成
+
#end
@@ -85,7 +84,7 @@
{{ dict.label }}
#else##没数据字典
- 请选择字典生成
+ 请选择字典生成
#end
diff --git a/yudao-module-infra/yudao-module-infra-biz/src/main/resources/codegen/vue3/views/components/form_sub_normal.vue.vm b/yudao-module-infra/yudao-module-infra-biz/src/main/resources/codegen/vue3/views/components/form_sub_normal.vue.vm
index dbd03569e..3fa1effb2 100644
--- a/yudao-module-infra/yudao-module-infra-biz/src/main/resources/codegen/vue3/views/components/form_sub_normal.vue.vm
+++ b/yudao-module-infra/yudao-module-infra-biz/src/main/resources/codegen/vue3/views/components/form_sub_normal.vue.vm
@@ -92,12 +92,11 @@
- {{ dict.label }}
-
+ :label="dict.label"
+ :value="dict.value"
+ />
#else##没数据字典
- 请选择字典生成
+
#end
@@ -117,7 +116,7 @@
{{ dict.label }}
#else##没数据字典
- 请选择字典生成
+ 请选择字典生成
#end
@@ -219,12 +218,11 @@
- {{ dict.label }}
-
+ :label="dict.label"
+ :value="dict.value"
+ />
#else##没数据字典
- 请选择字典生成
+
#end
@@ -240,7 +238,7 @@
{{ dict.label }}
#else##没数据字典
- 请选择字典生成
+ 请选择字典生成
#end
diff --git a/yudao-module-infra/yudao-module-infra-biz/src/main/resources/codegen/vue3/views/form.vue.vm b/yudao-module-infra/yudao-module-infra-biz/src/main/resources/codegen/vue3/views/form.vue.vm
index 8e3596b4f..e37474b85 100644
--- a/yudao-module-infra/yudao-module-infra-biz/src/main/resources/codegen/vue3/views/form.vue.vm
+++ b/yudao-module-infra/yudao-module-infra-biz/src/main/resources/codegen/vue3/views/form.vue.vm
@@ -75,12 +75,11 @@
- {{ dict.label }}
-
+ :label="dict.label"
+ :value="dict.value"
+ />
#else##没数据字典
- 请选择字典生成
+
#end
@@ -96,7 +95,7 @@
{{ dict.label }}
#else##没数据字典
- 请选择字典生成
+ 请选择字典生成
#end
diff --git a/yudao-module-mall/yudao-module-product-api/src/main/java/cn/iocoder/yudao/module/product/api/spu/dto/ProductSpuRespDTO.java b/yudao-module-mall/yudao-module-product-api/src/main/java/cn/iocoder/yudao/module/product/api/spu/dto/ProductSpuRespDTO.java
index 707ccc338..60fb0bcd1 100644
--- a/yudao-module-mall/yudao-module-product-api/src/main/java/cn/iocoder/yudao/module/product/api/spu/dto/ProductSpuRespDTO.java
+++ b/yudao-module-mall/yudao-module-product-api/src/main/java/cn/iocoder/yudao/module/product/api/spu/dto/ProductSpuRespDTO.java
@@ -3,6 +3,8 @@ package cn.iocoder.yudao.module.product.api.spu.dto;
import cn.iocoder.yudao.module.product.enums.spu.ProductSpuStatusEnum;
import lombok.Data;
+import java.util.List;
+
/**
* 商品 SPU 信息 Response DTO
*
@@ -68,6 +70,13 @@ public class ProductSpuRespDTO {
// ========== 物流相关字段 =========
+ /**
+ * 配送方式数组
+ *
+ * 对应 DeliveryTypeEnum 枚举
+ */
+ private List deliveryTypes;
+
/**
* 物流配置模板编号
*
diff --git a/yudao-module-mall/yudao-module-product-biz/src/main/java/cn/iocoder/yudao/module/product/service/comment/ProductCommentServiceImpl.java b/yudao-module-mall/yudao-module-product-biz/src/main/java/cn/iocoder/yudao/module/product/service/comment/ProductCommentServiceImpl.java
index 83c8e93a1..f12345416 100644
--- a/yudao-module-mall/yudao-module-product-biz/src/main/java/cn/iocoder/yudao/module/product/service/comment/ProductCommentServiceImpl.java
+++ b/yudao-module-mall/yudao-module-product-biz/src/main/java/cn/iocoder/yudao/module/product/service/comment/ProductCommentServiceImpl.java
@@ -67,7 +67,7 @@ public class ProductCommentServiceImpl implements ProductCommentService {
// 校验 SPU
ProductSpuDO spu = validateSpu(sku.getSpuId());
// 校验评论
- validateCommentExists(createReqDTO.getUserId(), createReqDTO.getOrderId());
+ validateCommentExists(createReqDTO.getUserId(), createReqDTO.getOrderItemId());
// 获取用户详细信息
MemberUserRespDTO user = memberUserApi.getUser(createReqDTO.getUserId());
diff --git a/yudao-module-mall/yudao-module-promotion-api/src/main/java/cn/iocoder/yudao/module/promotion/api/coupon/CouponApi.java b/yudao-module-mall/yudao-module-promotion-api/src/main/java/cn/iocoder/yudao/module/promotion/api/coupon/CouponApi.java
index 27e5b6fb8..789a4526d 100644
--- a/yudao-module-mall/yudao-module-promotion-api/src/main/java/cn/iocoder/yudao/module/promotion/api/coupon/CouponApi.java
+++ b/yudao-module-mall/yudao-module-promotion-api/src/main/java/cn/iocoder/yudao/module/promotion/api/coupon/CouponApi.java
@@ -3,9 +3,11 @@ package cn.iocoder.yudao.module.promotion.api.coupon;
import cn.iocoder.yudao.module.promotion.api.coupon.dto.CouponRespDTO;
import cn.iocoder.yudao.module.promotion.api.coupon.dto.CouponUseReqDTO;
import cn.iocoder.yudao.module.promotion.api.coupon.dto.CouponValidReqDTO;
-
import jakarta.validation.Valid;
+import java.util.List;
+import java.util.Map;
+
/**
* 优惠劵 API 接口
*
@@ -35,4 +37,21 @@ public interface CouponApi {
*/
CouponRespDTO validateCoupon(@Valid CouponValidReqDTO validReqDTO);
+ /**
+ * 【管理员】给指定用户批量发送优惠券
+ *
+ * @param giveCoupons key: 优惠劵模版编号,value:对应的数量
+ * @param userId 用户编号
+ * @return 优惠券编号列表
+ */
+ List takeCouponsByAdmin(Map giveCoupons, Long userId);
+
+ /**
+ * 【管理员】作废指定用户的指定优惠劵
+ *
+ * @param giveCouponIds 赠送的优惠券编号
+ * @param userId 用户编号
+ */
+ void invalidateCouponsByAdmin(List giveCouponIds, Long userId);
+
}
diff --git a/yudao-module-mall/yudao-module-promotion-api/src/main/java/cn/iocoder/yudao/module/promotion/api/reward/dto/RewardActivityMatchRespDTO.java b/yudao-module-mall/yudao-module-promotion-api/src/main/java/cn/iocoder/yudao/module/promotion/api/reward/dto/RewardActivityMatchRespDTO.java
index 6ae71a1d9..958668461 100644
--- a/yudao-module-mall/yudao-module-promotion-api/src/main/java/cn/iocoder/yudao/module/promotion/api/reward/dto/RewardActivityMatchRespDTO.java
+++ b/yudao-module-mall/yudao-module-promotion-api/src/main/java/cn/iocoder/yudao/module/promotion/api/reward/dto/RewardActivityMatchRespDTO.java
@@ -1,9 +1,14 @@
package cn.iocoder.yudao.module.promotion.api.reward.dto;
+import cn.iocoder.yudao.framework.common.enums.CommonStatusEnum;
import cn.iocoder.yudao.module.promotion.enums.common.PromotionConditionTypeEnum;
+import cn.iocoder.yudao.module.promotion.enums.common.PromotionProductScopeEnum;
import lombok.Data;
+import java.io.Serializable;
+import java.time.LocalDateTime;
import java.util.List;
+import java.util.Map;
/**
* 满减送活动的匹配 Response DTO
@@ -21,28 +26,50 @@ public class RewardActivityMatchRespDTO {
* 活动标题
*/
private String name;
+ /**
+ * 状态
+ *
+ * 枚举 {@link CommonStatusEnum}
+ */
+ private Integer status;
+ /**
+ * 开始时间
+ */
+ private LocalDateTime startTime;
+ /**
+ * 结束时间
+ */
+ private LocalDateTime endTime;
+ /**
+ * 备注
+ */
+ private String remark;
/**
* 条件类型
*
* 枚举 {@link PromotionConditionTypeEnum}
*/
private Integer conditionType;
+ /**
+ * 商品范围
+ *
+ * 枚举 {@link PromotionProductScopeEnum}
+ */
+ private Integer productScope;
+ /**
+ * 商品 SPU 编号的数组
+ */
+ private List productScopeValues;
/**
* 优惠规则的数组
*/
private List rules;
- /**
- * 商品 SPU 编号的数组
- */
- private List spuIds;
-
- // TODO 芋艿:后面 RewardActivityRespDTO 有了之后,Rule 可以放过去
/**
* 优惠规则
*/
@Data
- public static class Rule {
+ public static class Rule implements Serializable {
/**
* 优惠门槛
@@ -64,13 +91,14 @@ public class RewardActivityMatchRespDTO {
*/
private Integer point;
/**
- * 赠送的优惠劵编号的数组
+ * 赠送的优惠劵
+ *
+ * key: 优惠劵模版编号
+ * value:对应的优惠券数量
+ *
+ * 目的:用于订单支付后赠送优惠券
*/
- private List couponIds;
- /**
- * 赠送的优惠券数量的数组
- */
- private List couponCounts;
+ private Map giveCouponTemplateCounts;
}
diff --git a/yudao-module-mall/yudao-module-promotion-api/src/main/java/cn/iocoder/yudao/module/promotion/enums/ErrorCodeConstants.java b/yudao-module-mall/yudao-module-promotion-api/src/main/java/cn/iocoder/yudao/module/promotion/enums/ErrorCodeConstants.java
index 8cebd6e13..e1efb9c91 100644
--- a/yudao-module-mall/yudao-module-promotion-api/src/main/java/cn/iocoder/yudao/module/promotion/enums/ErrorCodeConstants.java
+++ b/yudao-module-mall/yudao-module-promotion-api/src/main/java/cn/iocoder/yudao/module/promotion/enums/ErrorCodeConstants.java
@@ -44,7 +44,8 @@ public interface ErrorCodeConstants {
ErrorCode REWARD_ACTIVITY_UPDATE_FAIL_STATUS_CLOSED = new ErrorCode(1_013_006_002, "满减送活动已关闭,不能修改");
ErrorCode REWARD_ACTIVITY_DELETE_FAIL_STATUS_NOT_CLOSED = new ErrorCode(1_013_006_003, "满减送活动未关闭,不能删除");
ErrorCode REWARD_ACTIVITY_CLOSE_FAIL_STATUS_CLOSED = new ErrorCode(1_013_006_004, "满减送活动已关闭,不能重复关闭");
- ErrorCode REWARD_ACTIVITY_CLOSE_FAIL_STATUS_END = new ErrorCode(1_013_006_005, "满减送活动已结束,不能关闭");
+ ErrorCode REWARD_ACTIVITY_SCOPE_ALL_EXISTS = new ErrorCode(1_013_006_005, "已存在商品范围为全场的满减送活动");
+ ErrorCode REWARD_ACTIVITY_SCOPE_CATEGORY_EXISTS = new ErrorCode(1_013_006_006, "存在商品类型参加了其它满减送活动");
// ========== TODO 空着 1-013-007-000 ============
diff --git a/yudao-module-mall/yudao-module-promotion-api/src/main/java/cn/iocoder/yudao/module/promotion/enums/common/PromotionProductScopeEnum.java b/yudao-module-mall/yudao-module-promotion-api/src/main/java/cn/iocoder/yudao/module/promotion/enums/common/PromotionProductScopeEnum.java
index 882dc4aee..4a95cb1fa 100644
--- a/yudao-module-mall/yudao-module-promotion-api/src/main/java/cn/iocoder/yudao/module/promotion/enums/common/PromotionProductScopeEnum.java
+++ b/yudao-module-mall/yudao-module-promotion-api/src/main/java/cn/iocoder/yudao/module/promotion/enums/common/PromotionProductScopeEnum.java
@@ -5,6 +5,7 @@ import lombok.AllArgsConstructor;
import lombok.Getter;
import java.util.Arrays;
+import java.util.Objects;
/**
* 营销的商品范围枚举
@@ -15,10 +16,9 @@ import java.util.Arrays;
@AllArgsConstructor
public enum PromotionProductScopeEnum implements IntArrayValuable {
- ALL(1, "通用券"), // 全部商品
- SPU(2, "商品券"), // 指定商品
- CATEGORY(3, "品类券"), // 指定品类
- ;
+ ALL(1, "全部商品"),
+ SPU(2, "指定商品"),
+ CATEGORY(3, "指定品类");
public static final int[] ARRAYS = Arrays.stream(values()).mapToInt(PromotionProductScopeEnum::getScope).toArray();
@@ -36,4 +36,16 @@ public enum PromotionProductScopeEnum implements IntArrayValuable {
return ARRAYS;
}
+ public static boolean isAll(Integer scope) {
+ return Objects.equals(scope, ALL.scope);
+ }
+
+ public static boolean isSpu(Integer scope) {
+ return Objects.equals(scope, SPU.scope);
+ }
+
+ public static boolean isCategory(Integer scope) {
+ return Objects.equals(scope, CATEGORY.scope);
+ }
+
}
diff --git a/yudao-module-mall/yudao-module-promotion-api/src/main/java/cn/iocoder/yudao/module/promotion/enums/coupon/CouponStatusEnum.java b/yudao-module-mall/yudao-module-promotion-api/src/main/java/cn/iocoder/yudao/module/promotion/enums/coupon/CouponStatusEnum.java
index 320345d85..bef4db225 100644
--- a/yudao-module-mall/yudao-module-promotion-api/src/main/java/cn/iocoder/yudao/module/promotion/enums/coupon/CouponStatusEnum.java
+++ b/yudao-module-mall/yudao-module-promotion-api/src/main/java/cn/iocoder/yudao/module/promotion/enums/coupon/CouponStatusEnum.java
@@ -17,8 +17,7 @@ public enum CouponStatusEnum implements IntArrayValuable {
UNUSED(1, "未使用"),
USED(2, "已使用"),
- EXPIRE(3, "已过期"),
- ;
+ EXPIRE(3, "已过期");
public static final int[] ARRAYS = Arrays.stream(values()).mapToInt(CouponStatusEnum::getStatus).toArray();
diff --git a/yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/api/coupon/CouponApiImpl.java b/yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/api/coupon/CouponApiImpl.java
index b7f904583..edc8f1b7f 100644
--- a/yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/api/coupon/CouponApiImpl.java
+++ b/yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/api/coupon/CouponApiImpl.java
@@ -7,10 +7,12 @@ import cn.iocoder.yudao.module.promotion.api.coupon.dto.CouponValidReqDTO;
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;
+import jakarta.annotation.Resource;
import org.springframework.stereotype.Service;
import org.springframework.validation.annotation.Validated;
-import jakarta.annotation.Resource;
+import java.util.List;
+import java.util.Map;
/**
* 优惠劵 API 实现类
@@ -41,4 +43,14 @@ public class CouponApiImpl implements CouponApi {
return CouponConvert.INSTANCE.convert(coupon);
}
+ @Override
+ public List takeCouponsByAdmin(Map giveCoupons, Long userId) {
+ return couponService.takeCouponsByAdmin(giveCoupons, userId);
+ }
+
+ @Override
+ public void invalidateCouponsByAdmin(List giveCouponIds, Long userId) {
+ couponService.invalidateCouponsByAdmin(giveCouponIds, userId);
+ }
+
}
diff --git a/yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/controller/admin/reward/RewardActivityController.java b/yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/controller/admin/reward/RewardActivityController.java
index d41912337..0e50ffc14 100755
--- a/yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/controller/admin/reward/RewardActivityController.java
+++ b/yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/controller/admin/reward/RewardActivityController.java
@@ -2,23 +2,22 @@ package cn.iocoder.yudao.module.promotion.controller.admin.reward;
import cn.iocoder.yudao.framework.common.pojo.CommonResult;
import cn.iocoder.yudao.framework.common.pojo.PageResult;
+import cn.iocoder.yudao.framework.common.util.object.BeanUtils;
import cn.iocoder.yudao.module.promotion.controller.admin.reward.vo.RewardActivityCreateReqVO;
import cn.iocoder.yudao.module.promotion.controller.admin.reward.vo.RewardActivityPageReqVO;
import cn.iocoder.yudao.module.promotion.controller.admin.reward.vo.RewardActivityRespVO;
import cn.iocoder.yudao.module.promotion.controller.admin.reward.vo.RewardActivityUpdateReqVO;
-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 io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.tags.Tag;
+import jakarta.annotation.Resource;
+import jakarta.validation.Valid;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.*;
-import jakarta.annotation.Resource;
-import jakarta.validation.Valid;
-
import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success;
@Tag(name = "管理后台 - 满减送活动")
@@ -69,7 +68,7 @@ public class RewardActivityController {
@PreAuthorize("@ss.hasPermission('promotion:reward-activity:query')")
public CommonResult getRewardActivity(@RequestParam("id") Long id) {
RewardActivityDO rewardActivity = rewardActivityService.getRewardActivity(id);
- return success(RewardActivityConvert.INSTANCE.convert(rewardActivity));
+ return success(BeanUtils.toBean(rewardActivity, RewardActivityRespVO.class));
}
@GetMapping("/page")
@@ -77,7 +76,7 @@ public class RewardActivityController {
@PreAuthorize("@ss.hasPermission('promotion:reward-activity:query')")
public CommonResult> getRewardActivityPage(@Valid RewardActivityPageReqVO pageVO) {
PageResult pageResult = rewardActivityService.getRewardActivityPage(pageVO);
- return success(RewardActivityConvert.INSTANCE.convertPage(pageResult));
+ return success(BeanUtils.toBean(pageResult, RewardActivityRespVO.class));
}
}
diff --git a/yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/controller/admin/reward/vo/RewardActivityBaseVO.java b/yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/controller/admin/reward/vo/RewardActivityBaseVO.java
index ae7a9f0bd..590e9a7f2 100755
--- a/yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/controller/admin/reward/vo/RewardActivityBaseVO.java
+++ b/yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/controller/admin/reward/vo/RewardActivityBaseVO.java
@@ -6,18 +6,17 @@ import cn.iocoder.yudao.module.promotion.enums.common.PromotionConditionTypeEnum
import cn.iocoder.yudao.module.promotion.enums.common.PromotionProductScopeEnum;
import com.fasterxml.jackson.annotation.JsonIgnore;
import io.swagger.v3.oas.annotations.media.Schema;
-import lombok.Data;
-import org.springframework.format.annotation.DateTimeFormat;
-
import jakarta.validation.Valid;
import jakarta.validation.constraints.AssertTrue;
import jakarta.validation.constraints.Future;
import jakarta.validation.constraints.Min;
import jakarta.validation.constraints.NotNull;
+import lombok.Data;
+
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;
+import java.util.Map;
+import java.util.Objects;
/**
* 满减送活动 Base VO,提供给添加、修改、详细的子 VO 使用
@@ -32,12 +31,10 @@ public class RewardActivityBaseVO {
@Schema(description = "开始时间", requiredMode = Schema.RequiredMode.REQUIRED)
@NotNull(message = "开始时间不能为空")
- @DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND)
private LocalDateTime startTime;
@Schema(description = "结束时间", requiredMode = Schema.RequiredMode.REQUIRED)
@NotNull(message = "结束时间不能为空")
- @DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND)
@Future(message = "结束时间必须大于当前时间")
private LocalDateTime endTime;
@@ -54,8 +51,8 @@ public class RewardActivityBaseVO {
@InEnum(value = PromotionProductScopeEnum.class, message = "商品范围必须是 {value}")
private Integer productScope;
- @Schema(description = "商品 SPU 编号的数组", example = "1,2,3")
- private List productSpuIds;
+ @Schema(description = "商品范围编号的数组", example = "[1, 3]")
+ private List productScopeValues;
/**
* 优惠规则的数组
@@ -76,24 +73,28 @@ public class RewardActivityBaseVO {
private Integer discountPrice;
@Schema(description = "是否包邮", requiredMode = Schema.RequiredMode.REQUIRED, example = "true")
+ @NotNull(message = "规则是否包邮不能为空")
private Boolean freeDelivery;
@Schema(description = "赠送的积分", requiredMode = Schema.RequiredMode.REQUIRED, example = "100")
- @Min(value = 1L, message = "赠送的积分必须大于等于 1")
private Integer point;
- @Schema(description = "赠送的优惠劵编号的数组", example = "1,2,3")
- private List couponIds;
+ @Schema(description = "赠送的优惠劵编号的数组")
+ private Map giveCouponTemplateCounts;
- @Schema(description = "赠送的优惠券数量的数组", example = "1,2,3")
- private List couponCounts;
-
- @AssertTrue(message = "优惠劵和数量必须一一对应")
+ @AssertTrue(message = "赠送的积分不能小于 0")
@JsonIgnore
- public boolean isCouponCountsValid() {
- return CollUtil.size(couponCounts) == CollUtil.size(couponCounts);
+ public boolean isPointValid() {
+ return point == null || point >= 0;
}
}
+ @AssertTrue(message = "商品范围编号的数组不能为空")
+ @JsonIgnore
+ public boolean isProductScopeValuesValid() {
+ return Objects.equals(productScope, PromotionProductScopeEnum.ALL.getScope()) // 全部范围时,可以为空
+ || CollUtil.isNotEmpty(productScopeValues);
+ }
+
}
diff --git a/yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/controller/app/activity/AppActivityController.java b/yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/controller/app/activity/AppActivityController.java
index 4ec685aab..fae7fa54d 100644
--- a/yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/controller/app/activity/AppActivityController.java
+++ b/yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/controller/app/activity/AppActivityController.java
@@ -2,8 +2,11 @@ package cn.iocoder.yudao.module.promotion.controller.app.activity;
import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.map.MapUtil;
+import cn.hutool.core.util.ObjUtil;
import cn.iocoder.yudao.framework.common.enums.CommonStatusEnum;
import cn.iocoder.yudao.framework.common.pojo.CommonResult;
+import cn.iocoder.yudao.module.product.api.spu.ProductSpuApi;
+import cn.iocoder.yudao.module.product.api.spu.dto.ProductSpuRespDTO;
import cn.iocoder.yudao.module.promotion.controller.app.activity.vo.AppActivityRespVO;
import cn.iocoder.yudao.module.promotion.dal.dataobject.bargain.BargainActivityDO;
import cn.iocoder.yudao.module.promotion.dal.dataobject.combination.CombinationActivityDO;
@@ -11,7 +14,7 @@ import cn.iocoder.yudao.module.promotion.dal.dataobject.discount.DiscountActivit
import cn.iocoder.yudao.module.promotion.dal.dataobject.discount.DiscountProductDO;
import cn.iocoder.yudao.module.promotion.dal.dataobject.reward.RewardActivityDO;
import cn.iocoder.yudao.module.promotion.dal.dataobject.seckill.SeckillActivityDO;
-import cn.iocoder.yudao.module.promotion.enums.common.PromotionActivityStatusEnum;
+import cn.iocoder.yudao.module.promotion.enums.common.PromotionProductScopeEnum;
import cn.iocoder.yudao.module.promotion.enums.common.PromotionTypeEnum;
import cn.iocoder.yudao.module.promotion.service.bargain.BargainActivityService;
import cn.iocoder.yudao.module.promotion.service.combination.CombinationActivityService;
@@ -30,7 +33,6 @@ 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.*;
@@ -52,6 +54,9 @@ public class AppActivityController {
@Resource
private RewardActivityService rewardActivityService;
+ @Resource
+ private ProductSpuApi productSpuApi;
+
@GetMapping("/list-by-spu-id")
@Operation(summary = "获得单个商品,近期参与的每个活动")
@Parameter(name = "spuId", description = "商品编号", required = true)
@@ -87,7 +92,7 @@ public class AppActivityController {
// 4. 限时折扣活动
getDiscountActivities(spuIds, now, activityList);
// 5. 满减送活动
- getRewardActivities(spuIds, now, activityList);
+ getRewardActivityList(spuIds, now, activityList);
return activityList;
}
@@ -144,28 +149,51 @@ public class AppActivityController {
item.getName(), productMap.get(item.getId()), item.getStartTime(), item.getEndTime())));
}
- private void getRewardActivities(Collection spuIds, LocalDateTime now, List activityList) {
- // TODO @puhui999:有 3 范围,不只 spuId,还有 categoryId,全部
- List rewardActivityList = rewardActivityService.getRewardActivityBySpuIdsAndStatusAndDateTimeLt(
- spuIds, PromotionActivityStatusEnum.RUN.getStatus(), now);
+ private void getRewardActivityList(Collection spuIds, LocalDateTime now, List activityList) {
+ // 1.1 获得所有的活动
+ List rewardActivityList = rewardActivityService.getRewardActivityListByStatusAndDateTimeLt(
+ CommonStatusEnum.ENABLE.getStatus(), now);
if (CollUtil.isEmpty(rewardActivityList)) {
return;
}
+ // 1.2 获得所有的商品信息
+ List spuList = productSpuApi.getSpuList(spuIds);
+ if (CollUtil.isEmpty(spuList)) {
+ return;
+ }
- Map> spuIdAndActivityMap = spuIds.stream()
- .collect(Collectors.toMap(
- spuId -> spuId,
- spuId -> rewardActivityList.stream()
- .filter(activity -> activity.getProductSpuIds().contains(spuId))
- .max(Comparator.comparing(RewardActivityDO::getCreateTime))));
- for (Long supId : spuIdAndActivityMap.keySet()) {
- if (spuIdAndActivityMap.get(supId).isEmpty()) {
+ // 2. 构建活动
+ for (RewardActivityDO rewardActivity : rewardActivityList) {
+ // 情况一:所有商品都能参加
+ if (PromotionProductScopeEnum.isAll(rewardActivity.getProductScope())) {
+ buildAppActivityRespVO(rewardActivity, spuIds, activityList);
+ }
+ // 情况二:指定商品参加
+ if (PromotionProductScopeEnum.isSpu(rewardActivity.getProductScope())) {
+ List fSpuIds = spuList.stream().map(ProductSpuRespDTO::getId).filter(id ->
+ rewardActivity.getProductScopeValues().contains(id)).toList();
+ buildAppActivityRespVO(rewardActivity, fSpuIds, activityList);
+ }
+ // 情况三:指定商品类型参加
+ if (PromotionProductScopeEnum.isCategory(rewardActivity.getProductScope())) {
+ List fSpuIds = spuList.stream().filter(spuItem -> rewardActivity.getProductScopeValues()
+ .contains(spuItem.getCategoryId())).map(ProductSpuRespDTO::getId).toList();
+ buildAppActivityRespVO(rewardActivity, fSpuIds, activityList);
+ }
+ }
+ }
+
+ private static void buildAppActivityRespVO(RewardActivityDO rewardActivity, Collection spuIds,
+ List activityList) {
+ for (Long spuId : spuIds) {
+ // 校验商品是否已经加入过活动
+ if (anyMatch(activityList, appActivity -> ObjUtil.equal(appActivity.getId(), rewardActivity.getId()) &&
+ ObjUtil.equal(appActivity.getSpuId(), spuId))) {
continue;
}
-
- RewardActivityDO rewardActivityDO = spuIdAndActivityMap.get(supId).get();
- activityList.add(new AppActivityRespVO(rewardActivityDO.getId(), PromotionTypeEnum.REWARD_ACTIVITY.getType(),
- rewardActivityDO.getName(), supId, rewardActivityDO.getStartTime(), rewardActivityDO.getEndTime()));
+ activityList.add(new AppActivityRespVO(rewardActivity.getId(),
+ PromotionTypeEnum.REWARD_ACTIVITY.getType(), rewardActivity.getName(), spuId,
+ rewardActivity.getStartTime(), rewardActivity.getEndTime()));
}
}
diff --git a/yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/convert/reward/RewardActivityConvert.java b/yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/convert/reward/RewardActivityConvert.java
deleted file mode 100755
index 5343656ed..000000000
--- a/yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/convert/reward/RewardActivityConvert.java
+++ /dev/null
@@ -1,29 +0,0 @@
-package cn.iocoder.yudao.module.promotion.convert.reward;
-
-import cn.iocoder.yudao.framework.common.pojo.PageResult;
-import cn.iocoder.yudao.module.promotion.controller.admin.reward.vo.RewardActivityCreateReqVO;
-import cn.iocoder.yudao.module.promotion.controller.admin.reward.vo.RewardActivityRespVO;
-import cn.iocoder.yudao.module.promotion.controller.admin.reward.vo.RewardActivityUpdateReqVO;
-import cn.iocoder.yudao.module.promotion.dal.dataobject.reward.RewardActivityDO;
-import org.mapstruct.Mapper;
-import org.mapstruct.factory.Mappers;
-
-/**
- * 满减送活动 Convert
- *
- * @author 芋道源码
- */
-@Mapper
-public interface RewardActivityConvert {
-
- RewardActivityConvert INSTANCE = Mappers.getMapper(RewardActivityConvert.class);
-
- RewardActivityDO convert(RewardActivityCreateReqVO bean);
-
- RewardActivityDO convert(RewardActivityUpdateReqVO bean);
-
- RewardActivityRespVO convert(RewardActivityDO bean);
-
- PageResult convertPage(PageResult page);
-
-}
diff --git a/yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/dal/dataobject/coupon/CouponDO.java b/yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/dal/dataobject/coupon/CouponDO.java
index 296d2a2fd..7182f0ea0 100644
--- a/yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/dal/dataobject/coupon/CouponDO.java
+++ b/yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/dal/dataobject/coupon/CouponDO.java
@@ -50,7 +50,6 @@ public class CouponDO extends BaseDO {
*
* 枚举 {@link CouponStatusEnum}
*/
- // TODO 芋艿:已作废?
private Integer status;
// TODO 芋艿:发放 adminid?
diff --git a/yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/dal/dataobject/reward/RewardActivityDO.java b/yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/dal/dataobject/reward/RewardActivityDO.java
index d94533e8c..a2f1e7e88 100644
--- a/yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/dal/dataobject/reward/RewardActivityDO.java
+++ b/yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/dal/dataobject/reward/RewardActivityDO.java
@@ -1,8 +1,8 @@
package cn.iocoder.yudao.module.promotion.dal.dataobject.reward;
+import cn.iocoder.yudao.framework.common.enums.CommonStatusEnum;
import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO;
import cn.iocoder.yudao.framework.mybatis.core.type.LongListTypeHandler;
-import cn.iocoder.yudao.module.promotion.enums.common.PromotionActivityStatusEnum;
import cn.iocoder.yudao.module.promotion.enums.common.PromotionConditionTypeEnum;
import cn.iocoder.yudao.module.promotion.enums.common.PromotionProductScopeEnum;
import com.baomidou.mybatisplus.annotation.KeySequence;
@@ -16,6 +16,7 @@ import lombok.EqualsAndHashCode;
import java.io.Serializable;
import java.time.LocalDateTime;
import java.util.List;
+import java.util.Map;
/**
* 满减送活动 DO
@@ -40,7 +41,7 @@ public class RewardActivityDO extends BaseDO {
/**
* 状态
*
- * 枚举 {@link PromotionActivityStatusEnum}
+ * 枚举 {@link CommonStatusEnum}
*/
private Integer status;
/**
@@ -71,7 +72,7 @@ public class RewardActivityDO extends BaseDO {
* 商品 SPU 编号的数组
*/
@TableField(typeHandler = LongListTypeHandler.class)
- private List productSpuIds;
+ private List productScopeValues;
/**
* 优惠规则的数组
*/
@@ -104,13 +105,14 @@ public class RewardActivityDO extends BaseDO {
*/
private Integer point;
/**
- * 赠送的优惠劵编号的数组
+ * 赠送的优惠劵
+ *
+ * key: 优惠劵模版编号
+ * value:对应的优惠券数量
+ *
+ * 目的:用于订单支付后赠送优惠券
*/
- private List couponIds;
- /**
- * 赠送的优惠券数量的数组
- */
- private List couponCounts;
+ private Map giveCouponTemplateCounts;
}
diff --git a/yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/dal/mysql/reward/RewardActivityMapper.java b/yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/dal/mysql/reward/RewardActivityMapper.java
index ca9e9668f..915696967 100755
--- a/yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/dal/mysql/reward/RewardActivityMapper.java
+++ b/yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/dal/mysql/reward/RewardActivityMapper.java
@@ -30,10 +30,6 @@ public interface RewardActivityMapper extends BaseMapperX {
.orderByDesc(RewardActivityDO::getId));
}
- default List selectListByStatus(Collection statuses) {
- return selectList(RewardActivityDO::getStatus, statuses);
- }
-
default List selectListByProductScopeAndStatus(Integer productScope, Integer status) {
return selectList(new LambdaQueryWrapperX()
.eq(RewardActivityDO::getProductScope, productScope)
@@ -53,16 +49,16 @@ public interface RewardActivityMapper extends BaseMapperX {
* 获取指定活动编号的活动列表且
* 开始时间和结束时间小于给定时间 dateTime 的活动列表
*
- * @param ids 活动编号
+ * @param status 状态
* @param dateTime 指定日期
* @return 活动列表
*/
- default List selectListByIdsAndDateTimeLt(Collection ids, LocalDateTime dateTime) {
+ default List selectListByStatusAndDateTimeLt(Integer status, LocalDateTime dateTime) {
return selectList(new LambdaQueryWrapperX()
- .in(RewardActivityDO::getId, ids)
+ .eq(RewardActivityDO::getStatus, status)
.lt(RewardActivityDO::getStartTime, dateTime)
.gt(RewardActivityDO::getEndTime, dateTime)// 开始时间 < 指定时间 < 结束时间,也就是说获取指定时间段的活动
- .orderByDesc(RewardActivityDO::getCreateTime)
+ .orderByAsc(RewardActivityDO::getStartTime)
);
}
diff --git a/yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/service/combination/CombinationRecordServiceImpl.java b/yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/service/combination/CombinationRecordServiceImpl.java
index cb70b8ea9..6f5ac3f62 100644
--- a/yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/service/combination/CombinationRecordServiceImpl.java
+++ b/yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/service/combination/CombinationRecordServiceImpl.java
@@ -27,6 +27,7 @@ import cn.iocoder.yudao.module.promotion.enums.combination.CombinationRecordStat
import cn.iocoder.yudao.module.system.api.social.SocialClientApi;
import cn.iocoder.yudao.module.system.api.social.dto.SocialWxaSubscribeMessageSendReqDTO;
import cn.iocoder.yudao.module.trade.api.order.TradeOrderApi;
+import cn.iocoder.yudao.module.trade.enums.order.TradeOrderCancelTypeEnum;
import jakarta.annotation.Nullable;
import jakarta.annotation.Resource;
import lombok.extern.slf4j.Slf4j;
@@ -37,7 +38,10 @@ import org.springframework.transaction.annotation.Transactional;
import org.springframework.validation.annotation.Validated;
import java.time.LocalDateTime;
-import java.util.*;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.List;
+import java.util.Map;
import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception;
import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.*;
@@ -335,7 +339,8 @@ public class CombinationRecordServiceImpl implements CombinationRecordService {
List headAndRecords = updateBatchCombinationRecords(headRecord,
CombinationRecordStatusEnum.FAILED);
// 2. 订单取消
- headAndRecords.forEach(item -> tradeOrderApi.cancelPaidOrder(item.getUserId(), item.getOrderId()));
+ headAndRecords.forEach(item -> tradeOrderApi.cancelPaidOrder(item.getUserId(), item.getOrderId(),
+ TradeOrderCancelTypeEnum.COMBINATION_CLOSE.getType()));
}
/**
diff --git a/yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/service/coupon/CouponService.java b/yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/service/coupon/CouponService.java
index edd654275..5fdcd0669 100644
--- a/yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/service/coupon/CouponService.java
+++ b/yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/service/coupon/CouponService.java
@@ -38,14 +38,6 @@ public interface CouponService {
*/
void validCoupon(CouponDO coupon);
- /**
- * 获得优惠劵分页
- *
- * @param pageReqVO 分页查询
- * @return 优惠劵分页
- */
- PageResult getCouponPage(CouponPageReqVO pageReqVO);
-
/**
* 使用优惠劵
*
@@ -69,42 +61,44 @@ public interface CouponService {
*/
void deleteCoupon(Long id);
- /**
- * 获得用户的优惠劵列表
- *
- * @param userId 用户编号
- * @param status 优惠劵状态
- * @return 优惠劵列表
- */
- List getCouponList(Long userId, Integer status);
-
- /**
- * 获得未使用的优惠劵数量
- *
- * @param userId 用户编号
- * @return 未使用的优惠劵数量
- */
- Long getUnusedCouponCount(Long userId);
-
/**
* 领取优惠券
*
* @param templateId 优惠券模板编号
* @param userIds 用户编号列表
* @param takeType 领取方式
+ * @return key: userId, value: 优惠券编号列表
*/
- void takeCoupon(Long templateId, Set userIds, CouponTakeTypeEnum takeType);
+ Map> takeCoupon(Long templateId, Set userIds, CouponTakeTypeEnum takeType);
/**
* 【管理员】给用户发送优惠券
*
* @param templateId 优惠券模板编号
* @param userIds 用户编号列表
+ * @return key: userId, value: 优惠券编号列表
*/
- default void takeCouponByAdmin(Long templateId, Set userIds) {
- takeCoupon(templateId, userIds, CouponTakeTypeEnum.ADMIN);
+ default Map> takeCouponByAdmin(Long templateId, Set userIds) {
+ return takeCoupon(templateId, userIds, CouponTakeTypeEnum.ADMIN);
}
+ /**
+ * 【管理员】给指定用户批量发送优惠券
+ *
+ * @param giveCoupons key: 优惠劵模版编号,value:对应的数量
+ * @param userId 用户编号
+ * @return 优惠券编号列表
+ */
+ List takeCouponsByAdmin(Map giveCoupons, Long userId);
+
+ /**
+ * 【管理员】作废指定用户的指定优惠劵
+ *
+ * @param giveCouponIds 赠送的优惠券编号
+ * @param userId 用户编号
+ */
+ void invalidateCouponsByAdmin(List giveCouponIds, Long userId);
+
/**
* 【会员】领取优惠券
*
@@ -122,6 +116,49 @@ public interface CouponService {
*/
void takeCouponByRegister(Long userId);
+ /**
+ * 过期优惠券
+ *
+ * @return 过期数量
+ */
+ int expireCoupon();
+
+ // ======================= 查询相关 =======================
+
+ /**
+ * 获得未使用的优惠劵数量
+ *
+ * @param userId 用户编号
+ * @return 未使用的优惠劵数量
+ */
+ Long getUnusedCouponCount(Long userId);
+
+ /**
+ * 获得优惠劵分页
+ *
+ * @param pageReqVO 分页查询
+ * @return 优惠劵分页
+ */
+ PageResult getCouponPage(CouponPageReqVO pageReqVO);
+
+ /**
+ * 获得用户的优惠劵列表
+ *
+ * @param userId 用户编号
+ * @param status 优惠劵状态
+ * @return 优惠劵列表
+ */
+ List getCouponList(Long userId, Integer status);
+
+ /**
+ * 统计会员领取优惠券的数量
+ *
+ * @param templateIds 优惠券模板编号列表
+ * @param userId 用户编号
+ * @return 领取优惠券的数量
+ */
+ Map getTakeCountMapByTemplateIds(Collection templateIds, Long userId);
+
/**
* 获取会员领取指定优惠券的数量
*
@@ -134,15 +171,6 @@ public interface CouponService {
return MapUtil.getInt(map, templateId, 0);
}
- /**
- * 统计会员领取优惠券的数量
- *
- * @param templateIds 优惠券模板编号列表
- * @param userId 用户编号
- * @return 领取优惠券的数量
- */
- Map getTakeCountMapByTemplateIds(Collection templateIds, Long userId);
-
/**
* 获取用户匹配的优惠券列表
*
@@ -152,13 +180,6 @@ public interface CouponService {
*/
List getMatchCouponList(Long userId, AppCouponMatchReqVO matchReqVO);
- /**
- * 过期优惠券
- *
- * @return 过期数量
- */
- int expireCoupon();
-
/**
* 获取用户是否可以领取优惠券
*
diff --git a/yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/service/coupon/CouponServiceImpl.java b/yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/service/coupon/CouponServiceImpl.java
index abf933d83..e6cd4ba0e 100644
--- a/yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/service/coupon/CouponServiceImpl.java
+++ b/yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/service/coupon/CouponServiceImpl.java
@@ -3,6 +3,7 @@ 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.ObjUtil;
import cn.hutool.core.util.ObjectUtil;
import cn.hutool.core.util.StrUtil;
import cn.hutool.extra.spring.SpringUtil;
@@ -19,19 +20,19 @@ 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 jakarta.annotation.Resource;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.validation.annotation.Validated;
-import jakarta.annotation.Resource;
-
import java.time.LocalDateTime;
import java.util.*;
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.*;
+import static cn.iocoder.yudao.framework.common.util.collection.MapUtils.findAndThen;
import static cn.iocoder.yudao.module.promotion.enums.ErrorCodeConstants.*;
import static java.util.Arrays.asList;
@@ -76,20 +77,6 @@ public class CouponServiceImpl implements CouponService {
}
}
- @Override
- public PageResult getCouponPage(CouponPageReqVO pageReqVO) {
- // 获得用户编号
- if (StrUtil.isNotEmpty(pageReqVO.getNickname())) {
- List users = memberUserApi.getUserListByNickname(pageReqVO.getNickname());
- if (CollUtil.isEmpty(users)) {
- return PageResult.empty();
- }
- pageReqVO.setUserIds(convertSet(users, MemberUserRespDTO::getId));
- }
- // 分页查询
- return couponMapper.selectPage(pageReqVO);
- }
-
@Override
public void useCoupon(Long id, Long userId, Long orderId) {
// 校验优惠劵
@@ -147,25 +134,8 @@ public class CouponServiceImpl implements CouponService {
}
@Override
- public List getCouponList(Long userId, Integer status) {
- return couponMapper.selectListByUserIdAndStatus(userId, status);
- }
-
- private CouponDO validateCouponExists(Long id) {
- CouponDO coupon = couponMapper.selectById(id);
- if (coupon == null) {
- throw exception(COUPON_NOT_EXISTS);
- }
- return coupon;
- }
-
- @Override
- public Long getUnusedCouponCount(Long userId) {
- return couponMapper.selectCountByUserIdAndStatus(userId, CouponStatusEnum.UNUSED.getStatus());
- }
-
- @Override
- public void takeCoupon(Long templateId, Set userIds, CouponTakeTypeEnum takeType) {
+ @Transactional(rollbackFor = Exception.class)
+ public Map> takeCoupon(Long templateId, Set userIds, CouponTakeTypeEnum takeType) {
CouponTemplateDO template = couponTemplateService.getCouponTemplate(templateId);
// 1. 过滤掉达到领取限制的用户
removeTakeLimitUser(userIds, template);
@@ -173,10 +143,77 @@ public class CouponServiceImpl implements CouponService {
validateCouponTemplateCanTake(template, userIds, takeType);
// 3. 批量保存优惠劵
- couponMapper.insertBatch(convertList(userIds, userId -> CouponConvert.INSTANCE.convert(template, userId)));
+ List couponList = convertList(userIds, userId -> CouponConvert.INSTANCE.convert(template, userId));
+ couponMapper.insertBatch(couponList);
- // 3. 增加优惠劵模板的领取数量
+ // 4. 增加优惠劵模板的领取数量
couponTemplateService.updateCouponTemplateTakeCount(templateId, userIds.size());
+
+ return convertMultiMap(couponList, CouponDO::getUserId, CouponDO::getId);
+ }
+
+ @Override
+ public List takeCouponsByAdmin(Map giveCoupons, Long userId) {
+ if (CollUtil.isEmpty(giveCoupons)) {
+ return Collections.emptyList();
+ }
+
+ List couponIds = new ArrayList<>();
+ // 循环发放
+ for (Map.Entry entry : giveCoupons.entrySet()) {
+ try {
+ for (int i = 0; i < entry.getValue(); i++) {
+ Map> userCouponIdsMap = getSelf().takeCoupon(entry.getKey(), CollUtil.newHashSet(userId),
+ CouponTakeTypeEnum.ADMIN);
+ findAndThen(userCouponIdsMap, userId, couponIds::addAll);
+ }
+ } catch (Exception e) {
+ log.error("[takeCouponsByAdmin][coupon({}) 优惠券发放失败]", entry, e);
+ }
+ }
+ return couponIds;
+ }
+
+ @Override
+ public void invalidateCouponsByAdmin(List giveCouponIds, Long userId) {
+ // 循环收回
+ for (Long couponId : giveCouponIds) {
+ try {
+ getSelf().invalidateCoupon(couponId, userId);
+ } catch (Exception e) {
+ log.error("[invalidateCouponsByAdmin][couponId({}) 收回优惠券失败]", couponId, e);
+ }
+ }
+ }
+
+ /**
+ * 【管理员】收回优惠券
+ *
+ * @param couponId 模版编号
+ * @param userId 用户编号
+ */
+ @Transactional(rollbackFor = Exception.class)
+ public void invalidateCoupon(Long couponId, Long userId) {
+ // 1.1 校验优惠券
+ CouponDO coupon = couponMapper.selectByIdAndUserId(couponId, userId);
+ if (coupon == null) {
+ throw exception(COUPON_NOT_EXISTS);
+ }
+ // 1.2 校验模板
+ CouponTemplateDO couponTemplate = couponTemplateService.getCouponTemplate(coupon.getTemplateId());
+ if (couponTemplate == null) {
+ throw exception(COUPON_TEMPLATE_NOT_EXISTS);
+ }
+ // 1.3 校验优惠券是否已经使用,如若使用则先不管
+ if (ObjUtil.equal(coupon.getStatus(), CouponStatusEnum.USED.getStatus())) {
+ log.info("[invalidateCoupon][coupon({}) 已经使用,无法作废]", couponId);
+ return;
+ }
+
+ // 2.1 减少优惠劵模板的领取数量
+ couponTemplateService.updateCouponTemplateTakeCount(couponTemplate.getId(), -1);
+ // 2.2 作废优惠劵
+ couponMapper.deleteById(couponId);
}
@Override
@@ -188,24 +225,6 @@ public class CouponServiceImpl implements CouponService {
}
}
- @Override
- public Map getTakeCountMapByTemplateIds(Collection templateIds, Long userId) {
- if (CollUtil.isEmpty(templateIds)) {
- return Collections.emptyMap();
- }
- return couponMapper.selectCountByUserIdAndTemplateIdIn(userId, templateIds);
- }
-
- @Override
- public List getMatchCouponList(Long userId, AppCouponMatchReqVO matchReqVO) {
- List list = couponMapper.selectListByUserIdAndStatusAndUsePriceLeAndProductScope(userId,
- CouponStatusEnum.UNUSED.getStatus(),
- matchReqVO.getPrice(), matchReqVO.getSpuIds(), matchReqVO.getCategoryIds());
- // 兜底逻辑:如果 CouponExpireJob 未执行,status 未变成 EXPIRE ,但是 validEndTime 已经过期了,需要进行过滤
- list.removeIf(coupon -> !LocalDateTimeUtils.isBetween(coupon.getValidStartTime(), coupon.getValidEndTime()));
- return list;
- }
-
@Override
public int expireCoupon() {
// 1. 查询待过期的优惠券
@@ -230,27 +249,6 @@ public class CouponServiceImpl implements CouponService {
return count;
}
- @Override
- public Map getUserCanCanTakeMap(Long userId, List templates) {
- // 1. 未登录时,都显示可以领取
- Map userCanTakeMap = convertMap(templates, CouponTemplateDO::getId, templateId -> true);
- if (userId == null) {
- return userCanTakeMap;
- }
-
- // 2.1 过滤领取数量无限制的
- Set templateIds = convertSet(templates, CouponTemplateDO::getId, template -> template.getTakeLimitCount() != -1);
- // 2.2 检查用户领取的数量是否超过限制
- if (CollUtil.isNotEmpty(templateIds)) {
- Map couponTakeCountMap = this.getTakeCountMapByTemplateIds(templateIds, userId);
- for (CouponTemplateDO template : templates) {
- Integer takeCount = couponTakeCountMap.get(template.getId());
- userCanTakeMap.put(template.getId(), takeCount == null || takeCount < template.getTakeLimitCount());
- }
- }
- return userCanTakeMap;
- }
-
/**
* 过期单个优惠劵
*
@@ -322,11 +320,84 @@ public class CouponServiceImpl implements CouponService {
userIds.removeIf(userId -> MapUtil.getInt(userTakeCountMap, userId, 0) >= couponTemplate.getTakeLimitCount());
}
+ //======================= 查询相关 =======================
+
+ @Override
+ public Long getUnusedCouponCount(Long userId) {
+ return couponMapper.selectCountByUserIdAndStatus(userId, CouponStatusEnum.UNUSED.getStatus());
+ }
+
+ @Override
+ public PageResult getCouponPage(CouponPageReqVO pageReqVO) {
+ // 获得用户编号
+ if (StrUtil.isNotEmpty(pageReqVO.getNickname())) {
+ List users = memberUserApi.getUserListByNickname(pageReqVO.getNickname());
+ if (CollUtil.isEmpty(users)) {
+ return PageResult.empty();
+ }
+ pageReqVO.setUserIds(convertSet(users, MemberUserRespDTO::getId));
+ }
+ // 分页查询
+ return couponMapper.selectPage(pageReqVO);
+ }
+
+ @Override
+ public List getCouponList(Long userId, Integer status) {
+ return couponMapper.selectListByUserIdAndStatus(userId, status);
+ }
+
+ @Override
+ public Map getTakeCountMapByTemplateIds(Collection templateIds, Long userId) {
+ if (CollUtil.isEmpty(templateIds)) {
+ return Collections.emptyMap();
+ }
+ return couponMapper.selectCountByUserIdAndTemplateIdIn(userId, templateIds);
+ }
+
+ @Override
+ public List getMatchCouponList(Long userId, AppCouponMatchReqVO matchReqVO) {
+ List list = couponMapper.selectListByUserIdAndStatusAndUsePriceLeAndProductScope(userId,
+ CouponStatusEnum.UNUSED.getStatus(),
+ matchReqVO.getPrice(), matchReqVO.getSpuIds(), matchReqVO.getCategoryIds());
+ // 兜底逻辑:如果 CouponExpireJob 未执行,status 未变成 EXPIRE ,但是 validEndTime 已经过期了,需要进行过滤
+ list.removeIf(coupon -> !LocalDateTimeUtils.isBetween(coupon.getValidStartTime(), coupon.getValidEndTime()));
+ return list;
+ }
+
+ @Override
+ public Map getUserCanCanTakeMap(Long userId, List templates) {
+ // 1. 未登录时,都显示可以领取
+ Map userCanTakeMap = convertMap(templates, CouponTemplateDO::getId, templateId -> true);
+ if (userId == null) {
+ return userCanTakeMap;
+ }
+
+ // 2.1 过滤领取数量无限制的
+ Set templateIds = convertSet(templates, CouponTemplateDO::getId, template -> template.getTakeLimitCount() != -1);
+ // 2.2 检查用户领取的数量是否超过限制
+ if (CollUtil.isNotEmpty(templateIds)) {
+ Map couponTakeCountMap = this.getTakeCountMapByTemplateIds(templateIds, userId);
+ for (CouponTemplateDO template : templates) {
+ Integer takeCount = couponTakeCountMap.get(template.getId());
+ userCanTakeMap.put(template.getId(), takeCount == null || takeCount < template.getTakeLimitCount());
+ }
+ }
+ return userCanTakeMap;
+ }
+
@Override
public CouponDO getCoupon(Long userId, Long id) {
return couponMapper.selectByIdAndUserId(id, userId);
}
+ private CouponDO validateCouponExists(Long id) {
+ CouponDO coupon = couponMapper.selectById(id);
+ if (coupon == null) {
+ throw exception(COUPON_NOT_EXISTS);
+ }
+ return coupon;
+ }
+
/**
* 获得自身的代理对象,解决 AOP 生效问题
*
@@ -335,4 +406,5 @@ public class CouponServiceImpl implements CouponService {
private CouponServiceImpl getSelf() {
return SpringUtil.getBean(getClass());
}
+
}
diff --git a/yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/service/discount/DiscountActivityServiceImpl.java b/yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/service/discount/DiscountActivityServiceImpl.java
index 25872d8e8..0c995267b 100644
--- a/yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/service/discount/DiscountActivityServiceImpl.java
+++ b/yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/service/discount/DiscountActivityServiceImpl.java
@@ -104,7 +104,10 @@ public class DiscountActivityServiceImpl implements DiscountActivityService {
}
// 计算新增的记录
List newDiscountProducts = convertList(updateReqVO.getProducts(),
- product -> DiscountActivityConvert.INSTANCE.convert(product).setActivityId(updateReqVO.getId()));
+ product -> DiscountActivityConvert.INSTANCE.convert(product)
+ .setActivityId(updateReqVO.getId())
+ .setActivityStartTime(updateReqVO.getStartTime())
+ .setActivityEndTime(updateReqVO.getEndTime()));
newDiscountProducts.removeIf(product -> dbDiscountProducts.stream().anyMatch(
dbProduct -> DiscountActivityConvert.INSTANCE.isEquals(dbProduct, product))); // 如果匹配到,说明是更新的
if (CollectionUtil.isNotEmpty(newDiscountProducts)) {
diff --git a/yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/service/reward/RewardActivityService.java b/yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/service/reward/RewardActivityService.java
index e2e225608..27cc86c33 100755
--- a/yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/service/reward/RewardActivityService.java
+++ b/yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/service/reward/RewardActivityService.java
@@ -75,11 +75,10 @@ public interface RewardActivityService {
/**
* 获取指定 spu 编号最近参加的活动,每个 spuId 只返回一条记录
*
- * @param spuIds spu 编号
* @param status 状态
* @param dateTime 当前日期时间
* @return 满减送活动列表
*/
- List getRewardActivityBySpuIdsAndStatusAndDateTimeLt(Collection spuIds, Integer status, LocalDateTime dateTime);
+ List getRewardActivityListByStatusAndDateTimeLt(Integer status, LocalDateTime dateTime);
}
diff --git a/yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/service/reward/RewardActivityServiceImpl.java b/yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/service/reward/RewardActivityServiceImpl.java
index e896eab92..eefbc6dee 100755
--- a/yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/service/reward/RewardActivityServiceImpl.java
+++ b/yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/service/reward/RewardActivityServiceImpl.java
@@ -1,15 +1,18 @@
package cn.iocoder.yudao.module.promotion.service.reward;
-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.common.util.object.BeanUtils;
+import cn.iocoder.yudao.module.product.api.category.ProductCategoryApi;
+import cn.iocoder.yudao.module.product.api.spu.ProductSpuApi;
import cn.iocoder.yudao.module.promotion.api.reward.dto.RewardActivityMatchRespDTO;
+import cn.iocoder.yudao.module.promotion.controller.admin.reward.vo.RewardActivityBaseVO;
import cn.iocoder.yudao.module.promotion.controller.admin.reward.vo.RewardActivityCreateReqVO;
import cn.iocoder.yudao.module.promotion.controller.admin.reward.vo.RewardActivityPageReqVO;
import cn.iocoder.yudao.module.promotion.controller.admin.reward.vo.RewardActivityUpdateReqVO;
-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.dal.mysql.reward.RewardActivityMapper;
-import cn.iocoder.yudao.module.promotion.enums.common.PromotionActivityStatusEnum;
+import cn.iocoder.yudao.module.promotion.enums.common.PromotionProductScopeEnum;
import cn.iocoder.yudao.module.promotion.util.PromotionUtils;
import jakarta.annotation.Resource;
import org.springframework.stereotype.Service;
@@ -17,13 +20,13 @@ 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 static cn.hutool.core.collection.CollUtil.intersectionDistinct;
import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception;
-import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertSet;
+import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.anyMatch;
import static cn.iocoder.yudao.module.promotion.enums.ErrorCodeConstants.*;
-import static java.util.Arrays.asList;
/**
* 满减送活动 Service 实现类
@@ -37,13 +40,20 @@ public class RewardActivityServiceImpl implements RewardActivityService {
@Resource
private RewardActivityMapper rewardActivityMapper;
+ @Resource
+ private ProductCategoryApi productCategoryApi;
+ @Resource
+ private ProductSpuApi productSpuApi;
+
@Override
public Long createRewardActivity(RewardActivityCreateReqVO createReqVO) {
- // 校验商品是否冲突
- validateRewardActivitySpuConflicts(null, createReqVO.getProductSpuIds());
+ // 1.1 校验商品范围
+ validateProductScope(createReqVO.getProductScope(), createReqVO.getProductScopeValues());
+ // 1.2 校验商品是否冲突
+ validateRewardActivitySpuConflicts(null, createReqVO);
- // 插入
- RewardActivityDO rewardActivity = RewardActivityConvert.INSTANCE.convert(createReqVO)
+ // 2. 插入
+ RewardActivityDO rewardActivity = BeanUtils.toBean(createReqVO, RewardActivityDO.class)
.setStatus(PromotionUtils.calculateActivityStatus(createReqVO.getEndTime()));
rewardActivityMapper.insert(rewardActivity);
// 返回
@@ -52,16 +62,18 @@ public class RewardActivityServiceImpl implements RewardActivityService {
@Override
public void updateRewardActivity(RewardActivityUpdateReqVO updateReqVO) {
- // 校验存在
+ // 1.1 校验存在
RewardActivityDO dbRewardActivity = validateRewardActivityExists(updateReqVO.getId());
- if (dbRewardActivity.getStatus().equals(PromotionActivityStatusEnum.CLOSE.getStatus())) { // 已关闭的活动,不能修改噢
+ if (dbRewardActivity.getStatus().equals(CommonStatusEnum.DISABLE.getStatus())) { // 已关闭的活动,不能修改噢
throw exception(REWARD_ACTIVITY_UPDATE_FAIL_STATUS_CLOSED);
}
- // 校验商品是否冲突
- validateRewardActivitySpuConflicts(updateReqVO.getId(), updateReqVO.getProductSpuIds());
+ // 1.2 校验商品范围
+ validateProductScope(updateReqVO.getProductScope(), updateReqVO.getProductScopeValues());
+ // 1.3 校验商品是否冲突
+ validateRewardActivitySpuConflicts(updateReqVO.getId(), updateReqVO);
- // 更新
- RewardActivityDO updateObj = RewardActivityConvert.INSTANCE.convert(updateReqVO)
+ // 2. 更新
+ RewardActivityDO updateObj = BeanUtils.toBean(updateReqVO, RewardActivityDO.class)
.setStatus(PromotionUtils.calculateActivityStatus(updateReqVO.getEndTime()));
rewardActivityMapper.updateById(updateObj);
}
@@ -70,15 +82,12 @@ public class RewardActivityServiceImpl implements RewardActivityService {
public void closeRewardActivity(Long id) {
// 校验存在
RewardActivityDO dbRewardActivity = validateRewardActivityExists(id);
- if (dbRewardActivity.getStatus().equals(PromotionActivityStatusEnum.CLOSE.getStatus())) { // 已关闭的活动,不能关闭噢
+ if (dbRewardActivity.getStatus().equals(CommonStatusEnum.DISABLE.getStatus())) { // 已关闭的活动,不能关闭噢
throw exception(REWARD_ACTIVITY_CLOSE_FAIL_STATUS_CLOSED);
}
- if (dbRewardActivity.getStatus().equals(PromotionActivityStatusEnum.END.getStatus())) { // 已关闭的活动,不能关闭噢
- throw exception(REWARD_ACTIVITY_CLOSE_FAIL_STATUS_END);
- }
// 更新
- RewardActivityDO updateObj = new RewardActivityDO().setId(id).setStatus(PromotionActivityStatusEnum.CLOSE.getStatus());
+ RewardActivityDO updateObj = new RewardActivityDO().setId(id).setStatus(CommonStatusEnum.DISABLE.getStatus());
rewardActivityMapper.updateById(updateObj);
}
@@ -86,7 +95,7 @@ public class RewardActivityServiceImpl implements RewardActivityService {
public void deleteRewardActivity(Long id) {
// 校验存在
RewardActivityDO dbRewardActivity = validateRewardActivityExists(id);
- if (!dbRewardActivity.getStatus().equals(PromotionActivityStatusEnum.CLOSE.getStatus())) { // 未关闭的活动,不能删除噢
+ if (dbRewardActivity.getStatus().equals(CommonStatusEnum.ENABLE.getStatus())) { // 未关闭的活动,不能删除噢
throw exception(REWARD_ACTIVITY_DELETE_FAIL_STATUS_NOT_CLOSED);
}
@@ -102,41 +111,39 @@ public class RewardActivityServiceImpl implements RewardActivityService {
return activity;
}
- // TODO @芋艿:逻辑有问题,需要优化;要分成全场、和指定来校验;
-
/**
* 校验商品参加的活动是否冲突
*
- * @param id 活动编号
- * @param spuIds 商品 SPU 编号数组
+ * @param id 活动编号
+ * @param rewardActivity 请求
*/
- private void validateRewardActivitySpuConflicts(Long id, Collection spuIds) {
- if (CollUtil.isEmpty(spuIds)) {
- return;
- }
- // 查询商品参加的活动
- List rewardActivityList = getRewardActivityListBySpuIds(spuIds,
- asList(PromotionActivityStatusEnum.WAIT.getStatus(), PromotionActivityStatusEnum.RUN.getStatus()));
+ private void validateRewardActivitySpuConflicts(Long id, RewardActivityBaseVO rewardActivity) {
+ List list = rewardActivityMapper.selectList(RewardActivityDO::getProductScope,
+ rewardActivity.getProductScope(), RewardActivityDO::getStatus, CommonStatusEnum.ENABLE.getStatus());
if (id != null) { // 排除自己这个活动
- rewardActivityList.removeIf(activity -> id.equals(activity.getId()));
+ list.removeIf(activity -> id.equals(activity.getId()));
}
- // 如果非空,则说明冲突
- if (CollUtil.isNotEmpty(rewardActivityList)) {
- throw exception(REWARD_ACTIVITY_SPU_CONFLICTS);
+
+ // 情况一:全部商品参加
+ if (PromotionProductScopeEnum.isAll(rewardActivity.getProductScope()) && !list.isEmpty()) {
+ throw exception(REWARD_ACTIVITY_SCOPE_ALL_EXISTS);
+ }
+ if (PromotionProductScopeEnum.isSpu(rewardActivity.getProductScope()) || // 情况二:指定商品参加
+ PromotionProductScopeEnum.isCategory(rewardActivity.getProductScope())) { // 情况三:指定商品类型参加
+ if (anyMatch(list, item -> !intersectionDistinct(item.getProductScopeValues(),
+ rewardActivity.getProductScopeValues()).isEmpty())) {
+ throw exception(PromotionProductScopeEnum.isSpu(rewardActivity.getProductScope()) ?
+ REWARD_ACTIVITY_SPU_CONFLICTS : REWARD_ACTIVITY_SCOPE_CATEGORY_EXISTS);
+ }
}
}
- /**
- * 获得商品参加的满减送活动的数组
- *
- * @param spuIds 商品 SPU 编号数组
- * @param statuses 活动状态数组
- * @return 商品参加的满减送活动的数组
- */
- private List getRewardActivityListBySpuIds(Collection spuIds,
- Collection statuses) {
- List list = rewardActivityMapper.selectListByStatus(statuses);
- return CollUtil.filter(list, activity -> CollUtil.containsAny(activity.getProductSpuIds(), spuIds));
+ private void validateProductScope(Integer productScope, List productScopeValues) {
+ if (Objects.equals(PromotionProductScopeEnum.SPU.getScope(), productScope)) {
+ productSpuApi.validateSpuList(productScopeValues);
+ } else if (Objects.equals(PromotionProductScopeEnum.CATEGORY.getScope(), productScope)) {
+ productCategoryApi.validateCategoryList(productScopeValues);
+ }
}
@Override
@@ -151,32 +158,13 @@ public class RewardActivityServiceImpl implements RewardActivityService {
@Override
public List getMatchRewardActivityList(Collection spuIds) {
- // TODO 芋艿:待实现;先指定,然后再全局的;
-// // 如果有全局活动,则直接选择它
-// List allActivities = rewardActivityMapper.selectListByProductScopeAndStatus(
-// PromotionProductScopeEnum.ALL.getScope(), PromotionActivityStatusEnum.RUN.getStatus());
-// if (CollUtil.isNotEmpty(allActivities)) {
-// return MapUtil.builder(allActivities.get(0), spuIds).build();
-// }
-//
-// // 查询某个活动参加的活动
-// List productActivityList = getRewardActivityListBySpuIds(spuIds,
-// singleton(PromotionActivityStatusEnum.RUN.getStatus()));
-// return convertMap(productActivityList, activity -> activity,
-// rewardActivityDO -> intersectionDistinct(rewardActivityDO.getProductSpuIds(), spuIds)); // 求交集返回
- return null;
+ List list = rewardActivityMapper.selectListBySpuIdsAndStatus(spuIds, CommonStatusEnum.ENABLE.getStatus());
+ return BeanUtils.toBean(list, RewardActivityMatchRespDTO.class);
}
@Override
- public List getRewardActivityBySpuIdsAndStatusAndDateTimeLt(Collection spuIds, Integer status, LocalDateTime dateTime) {
- // 1. 查询出指定 spuId 的 spu 参加的活动
- List rewardActivityList = rewardActivityMapper.selectListBySpuIdsAndStatus(spuIds, status);
- if (CollUtil.isEmpty(rewardActivityList)) {
- return Collections.emptyList();
- }
-
- // 2. 查询活动详情
- return rewardActivityMapper.selectListByIdsAndDateTimeLt(convertSet(rewardActivityList, RewardActivityDO::getId), dateTime);
+ public List getRewardActivityListByStatusAndDateTimeLt(Integer status, LocalDateTime dateTime) {
+ return rewardActivityMapper.selectListByStatusAndDateTimeLt(status, dateTime);
}
}
diff --git a/yudao-module-mall/yudao-module-promotion-biz/src/test/java/cn/iocoder/yudao/module/promotion/service/reward/RewardActivityServiceImplTest.java b/yudao-module-mall/yudao-module-promotion-biz/src/test/java/cn/iocoder/yudao/module/promotion/service/reward/RewardActivityServiceImplTest.java
index f2297abf6..7e7cf14db 100755
--- a/yudao-module-mall/yudao-module-promotion-biz/src/test/java/cn/iocoder/yudao/module/promotion/service/reward/RewardActivityServiceImplTest.java
+++ b/yudao-module-mall/yudao-module-promotion-biz/src/test/java/cn/iocoder/yudao/module/promotion/service/reward/RewardActivityServiceImplTest.java
@@ -1,21 +1,23 @@
package cn.iocoder.yudao.module.promotion.service.reward;
+import cn.iocoder.yudao.framework.common.enums.CommonStatusEnum;
import cn.iocoder.yudao.framework.common.pojo.PageResult;
import cn.iocoder.yudao.framework.test.core.ut.BaseDbUnitTest;
+import cn.iocoder.yudao.module.promotion.api.reward.dto.RewardActivityMatchRespDTO;
import cn.iocoder.yudao.module.promotion.controller.admin.reward.vo.RewardActivityCreateReqVO;
import cn.iocoder.yudao.module.promotion.controller.admin.reward.vo.RewardActivityPageReqVO;
import cn.iocoder.yudao.module.promotion.controller.admin.reward.vo.RewardActivityUpdateReqVO;
import cn.iocoder.yudao.module.promotion.dal.dataobject.reward.RewardActivityDO;
import cn.iocoder.yudao.module.promotion.dal.mysql.reward.RewardActivityMapper;
-import cn.iocoder.yudao.module.promotion.enums.common.PromotionActivityStatusEnum;
import cn.iocoder.yudao.module.promotion.enums.common.PromotionConditionTypeEnum;
import cn.iocoder.yudao.module.promotion.enums.common.PromotionProductScopeEnum;
+import jakarta.annotation.Resource;
import org.junit.jupiter.api.Disabled;
import org.junit.jupiter.api.Test;
import org.springframework.context.annotation.Import;
-import jakarta.annotation.Resource;
import java.time.Duration;
+import java.util.List;
import java.util.Set;
import static cn.hutool.core.util.RandomUtil.randomEle;
@@ -27,15 +29,15 @@ import static cn.iocoder.yudao.framework.test.core.util.AssertUtils.assertServic
import static cn.iocoder.yudao.framework.test.core.util.RandomUtils.randomLongId;
import static cn.iocoder.yudao.framework.test.core.util.RandomUtils.randomPojo;
import static cn.iocoder.yudao.module.promotion.enums.ErrorCodeConstants.REWARD_ACTIVITY_NOT_EXISTS;
-import static java.util.Arrays.asList;
+import static com.google.common.primitives.Longs.asList;
import static java.util.Collections.singletonList;
import static org.junit.jupiter.api.Assertions.*;
/**
-* {@link RewardActivityServiceImpl} 的单元测试类
-*
-* @author 芋道源码
-*/
+ * {@link RewardActivityServiceImpl} 的单元测试类
+ *
+ * @author 芋道源码
+ */
@Disabled // TODO 芋艿:后续 fix 补充的单测
@Import(RewardActivityServiceImpl.class)
public class RewardActivityServiceImplTest extends BaseDbUnitTest {
@@ -63,7 +65,7 @@ public class RewardActivityServiceImplTest extends BaseDbUnitTest {
// 校验记录的属性是否正确
RewardActivityDO rewardActivity = rewardActivityMapper.selectById(rewardActivityId);
assertPojoEquals(reqVO, rewardActivity, "rules");
- assertEquals(rewardActivity.getStatus(), PromotionActivityStatusEnum.WAIT.getStatus());
+ assertEquals(rewardActivity.getStatus(), CommonStatusEnum.DISABLE.getStatus());
for (int i = 0; i < reqVO.getRules().size(); i++) {
assertPojoEquals(reqVO.getRules().get(i), rewardActivity.getRules().get(i));
}
@@ -72,7 +74,7 @@ public class RewardActivityServiceImplTest extends BaseDbUnitTest {
@Test
public void testUpdateRewardActivity_success() {
// mock 数据
- RewardActivityDO dbRewardActivity = randomPojo(RewardActivityDO.class, o -> o.setStatus(PromotionActivityStatusEnum.WAIT.getStatus()));
+ RewardActivityDO dbRewardActivity = randomPojo(RewardActivityDO.class, o -> o.setStatus(CommonStatusEnum.DISABLE.getStatus()));
rewardActivityMapper.insert(dbRewardActivity);// @Sql: 先插入出一条存在的数据
// 准备参数
RewardActivityUpdateReqVO reqVO = randomPojo(RewardActivityUpdateReqVO.class, o -> {
@@ -88,7 +90,7 @@ public class RewardActivityServiceImplTest extends BaseDbUnitTest {
// 校验是否更新正确
RewardActivityDO rewardActivity = rewardActivityMapper.selectById(reqVO.getId()); // 获取最新的
assertPojoEquals(reqVO, rewardActivity, "rules");
- assertEquals(rewardActivity.getStatus(), PromotionActivityStatusEnum.WAIT.getStatus());
+ assertEquals(rewardActivity.getStatus(), CommonStatusEnum.DISABLE.getStatus());
for (int i = 0; i < reqVO.getRules().size(); i++) {
assertPojoEquals(reqVO.getRules().get(i), rewardActivity.getRules().get(i));
}
@@ -97,7 +99,7 @@ public class RewardActivityServiceImplTest extends BaseDbUnitTest {
@Test
public void testCloseRewardActivity() {
// mock 数据
- RewardActivityDO dbRewardActivity = randomPojo(RewardActivityDO.class, o -> o.setStatus(PromotionActivityStatusEnum.WAIT.getStatus()));
+ RewardActivityDO dbRewardActivity = randomPojo(RewardActivityDO.class, o -> o.setStatus(CommonStatusEnum.DISABLE.getStatus()));
rewardActivityMapper.insert(dbRewardActivity);// @Sql: 先插入出一条存在的数据
// 准备参数
Long id = dbRewardActivity.getId();
@@ -106,7 +108,7 @@ public class RewardActivityServiceImplTest extends BaseDbUnitTest {
rewardActivityService.closeRewardActivity(id);
// 校验状态
RewardActivityDO rewardActivity = rewardActivityMapper.selectById(id);
- assertEquals(rewardActivity.getStatus(), PromotionActivityStatusEnum.CLOSE.getStatus());
+ assertEquals(rewardActivity.getStatus(), CommonStatusEnum.DISABLE.getStatus());
}
@Test
@@ -121,15 +123,15 @@ public class RewardActivityServiceImplTest extends BaseDbUnitTest {
@Test
public void testDeleteRewardActivity_success() {
// mock 数据
- RewardActivityDO dbRewardActivity = randomPojo(RewardActivityDO.class, o -> o.setStatus(PromotionActivityStatusEnum.CLOSE.getStatus()));
+ RewardActivityDO dbRewardActivity = randomPojo(RewardActivityDO.class, o -> o.setStatus(CommonStatusEnum.DISABLE.getStatus()));
rewardActivityMapper.insert(dbRewardActivity);// @Sql: 先插入出一条存在的数据
// 准备参数
Long id = dbRewardActivity.getId();
// 调用
rewardActivityService.deleteRewardActivity(id);
- // 校验数据不存在了
- assertNull(rewardActivityMapper.selectById(id));
+ // 校验数据不存在了
+ assertNull(rewardActivityMapper.selectById(id));
}
@Test
@@ -143,77 +145,82 @@ public class RewardActivityServiceImplTest extends BaseDbUnitTest {
@Test
public void testGetRewardActivityPage() {
- // mock 数据
- RewardActivityDO dbRewardActivity = randomPojo(RewardActivityDO.class, o -> { // 等会查询到
- o.setName("芋艿");
- o.setStatus(PromotionActivityStatusEnum.CLOSE.getStatus());
- });
- rewardActivityMapper.insert(dbRewardActivity);
- // 测试 name 不匹配
- rewardActivityMapper.insert(cloneIgnoreId(dbRewardActivity, o -> o.setName("土豆")));
- // 测试 status 不匹配
- rewardActivityMapper.insert(cloneIgnoreId(dbRewardActivity, o -> o.setStatus(PromotionActivityStatusEnum.RUN.getStatus())));
- // 准备参数
- RewardActivityPageReqVO reqVO = new RewardActivityPageReqVO();
- reqVO.setName("芋艿");
- reqVO.setStatus(PromotionActivityStatusEnum.CLOSE.getStatus());
+ // mock 数据
+ RewardActivityDO dbRewardActivity = randomPojo(RewardActivityDO.class, o -> { // 等会查询到
+ o.setName("芋艿");
+ o.setStatus(CommonStatusEnum.DISABLE.getStatus());
+ });
+ rewardActivityMapper.insert(dbRewardActivity);
+ // 测试 name 不匹配
+ rewardActivityMapper.insert(cloneIgnoreId(dbRewardActivity, o -> o.setName("土豆")));
+ // 测试 status 不匹配
+ rewardActivityMapper.insert(cloneIgnoreId(dbRewardActivity, o -> o.setStatus(CommonStatusEnum.ENABLE.getStatus())));
+ // 准备参数
+ RewardActivityPageReqVO reqVO = new RewardActivityPageReqVO();
+ reqVO.setName("芋艿");
+ reqVO.setStatus(CommonStatusEnum.DISABLE.getStatus());
- // 调用
- PageResult pageResult = rewardActivityService.getRewardActivityPage(reqVO);
- // 断言
- assertEquals(1, pageResult.getTotal());
- assertEquals(1, pageResult.getList().size());
- assertPojoEquals(dbRewardActivity, pageResult.getList().get(0), "rules");
+ // 调用
+ PageResult pageResult = rewardActivityService.getRewardActivityPage(reqVO);
+ // 断言
+ assertEquals(1, pageResult.getTotal());
+ assertEquals(1, pageResult.getList().size());
+ assertPojoEquals(dbRewardActivity, pageResult.getList().get(0), "rules");
}
@Test
public void testGetRewardActivities_all() {
// mock 数据
- RewardActivityDO allActivity = randomPojo(RewardActivityDO.class, o -> o.setStatus(PromotionActivityStatusEnum.RUN.getStatus())
+ RewardActivityDO allActivity = randomPojo(RewardActivityDO.class, o -> o.setStatus(CommonStatusEnum.ENABLE.getStatus())
.setProductScope(PromotionProductScopeEnum.ALL.getScope()));
rewardActivityMapper.insert(allActivity);
- RewardActivityDO productActivity = randomPojo(RewardActivityDO.class, o -> o.setStatus(PromotionActivityStatusEnum.RUN.getStatus())
- .setProductScope(PromotionProductScopeEnum.SPU.getScope()).setProductSpuIds(asList(1L, 2L)));
+ RewardActivityDO productActivity = randomPojo(RewardActivityDO.class, o -> o.setStatus(CommonStatusEnum.ENABLE.getStatus())
+ .setProductScope(PromotionProductScopeEnum.SPU.getScope()).setProductScopeValues(asList(1L, 2L)));
rewardActivityMapper.insert(productActivity);
// 准备参数
Set spuIds = asSet(1L, 2L);
// 调用 TODO getMatchRewardActivities 没有这个方法,但是找到了 getMatchRewardActivityList
- //Map> matchRewardActivities = rewardActivityService.getMatchRewardActivities(spuIds);
+ List matchRewardActivityList = rewardActivityService.getMatchRewardActivityList(spuIds);
// 断言
- //assertEquals(matchRewardActivities.size(), 1);
- //Map.Entry> next = matchRewardActivities.entrySet().iterator().next();
- //assertPojoEquals(next.getKey(), allActivity);
- //assertEquals(next.getValue(), spuIds);
+ assertEquals(matchRewardActivityList.size(), 1);
+ matchRewardActivityList.forEach((activity) -> {
+ if (activity.getId().equals(productActivity.getId())) {
+ assertPojoEquals(activity, productActivity);
+ assertEquals(activity.getProductScopeValues(), asList(1L, 2L));
+ } else {
+ fail();
+ }
+ });
}
@Test
public void testGetRewardActivities_product() {
// mock 数据
- RewardActivityDO productActivity01 = randomPojo(RewardActivityDO.class, o -> o.setStatus(PromotionActivityStatusEnum.RUN.getStatus())
- .setProductScope(PromotionProductScopeEnum.SPU.getScope()).setProductSpuIds(asList(1L, 2L)));
+ RewardActivityDO productActivity01 = randomPojo(RewardActivityDO.class, o -> o.setStatus(CommonStatusEnum.ENABLE.getStatus())
+ .setProductScope(PromotionProductScopeEnum.SPU.getScope()).setProductScopeValues(asList(1L, 2L)));
rewardActivityMapper.insert(productActivity01);
- RewardActivityDO productActivity02 = randomPojo(RewardActivityDO.class, o -> o.setStatus(PromotionActivityStatusEnum.RUN.getStatus())
- .setProductScope(PromotionProductScopeEnum.SPU.getScope()).setProductSpuIds(singletonList(3L)));
+ RewardActivityDO productActivity02 = randomPojo(RewardActivityDO.class, o -> o.setStatus(CommonStatusEnum.ENABLE.getStatus())
+ .setProductScope(PromotionProductScopeEnum.SPU.getScope()).setProductScopeValues(singletonList(3L)));
rewardActivityMapper.insert(productActivity02);
// 准备参数
Set spuIds = asSet(1L, 2L, 3L);
// 调用 TODO getMatchRewardActivities 没有这个方法,但是找到了 getMatchRewardActivityList
- //Map> matchRewardActivities = rewardActivityService.getMatchRewardActivities(spuIds);
+ List matchRewardActivityList = rewardActivityService.getMatchRewardActivityList(spuIds);
// 断言
- //assertEquals(matchRewardActivities.size(), 2);
- //matchRewardActivities.forEach((activity, activitySpuIds) -> {
- // if (activity.getId().equals(productActivity01.getId())) {
- // assertPojoEquals(activity, productActivity01);
- // assertEquals(activitySpuIds, asSet(1L, 2L));
- // } else if (activity.getId().equals(productActivity02.getId())) {
- // assertPojoEquals(activity, productActivity02);
- // assertEquals(activitySpuIds, asSet(3L));
- // } else {
- // fail();
- // }
- //});
+ assertEquals(matchRewardActivityList.size(), 2);
+ matchRewardActivityList.forEach((activity) -> {
+ if (activity.getId().equals(productActivity01.getId())) {
+ assertPojoEquals(activity, productActivity01);
+ assertEquals(activity.getProductScopeValues(), asList(1L, 2L));
+ } else if (activity.getId().equals(productActivity02.getId())) {
+ assertPojoEquals(activity, productActivity02);
+ assertEquals(activity.getProductScopeValues(), singletonList(3L));
+ } else {
+ fail();
+ }
+ });
}
}
diff --git a/yudao-module-mall/yudao-module-trade-api/src/main/java/cn/iocoder/yudao/module/trade/api/order/TradeOrderApi.java b/yudao-module-mall/yudao-module-trade-api/src/main/java/cn/iocoder/yudao/module/trade/api/order/TradeOrderApi.java
index 744a7b8fd..4bf1f5bf9 100644
--- a/yudao-module-mall/yudao-module-trade-api/src/main/java/cn/iocoder/yudao/module/trade/api/order/TradeOrderApi.java
+++ b/yudao-module-mall/yudao-module-trade-api/src/main/java/cn/iocoder/yudao/module/trade/api/order/TradeOrderApi.java
@@ -28,13 +28,13 @@ public interface TradeOrderApi {
*/
TradeOrderRespDTO getOrder(Long id);
- // TODO 芋艿:需要优化下;
/**
* 取消支付订单
*
- * @param userId 用户编号
+ * @param userId 用户编号
* @param orderId 订单编号
+ * @param cancelType 取消类型
*/
- void cancelPaidOrder(Long userId, Long orderId);
+ void cancelPaidOrder(Long userId, Long orderId, Integer cancelType);
}
diff --git a/yudao-module-mall/yudao-module-trade-api/src/main/java/cn/iocoder/yudao/module/trade/enums/ErrorCodeConstants.java b/yudao-module-mall/yudao-module-trade-api/src/main/java/cn/iocoder/yudao/module/trade/enums/ErrorCodeConstants.java
index 33081d461..5613cae8e 100644
--- a/yudao-module-mall/yudao-module-trade-api/src/main/java/cn/iocoder/yudao/module/trade/enums/ErrorCodeConstants.java
+++ b/yudao-module-mall/yudao-module-trade-api/src/main/java/cn/iocoder/yudao/module/trade/enums/ErrorCodeConstants.java
@@ -35,6 +35,7 @@ public interface ErrorCodeConstants {
ErrorCode ORDER_RECEIVE_FAIL_DELIVERY_TYPE_NOT_PICK_UP = new ErrorCode(1_011_000_030, "交易订单自提失败,收货方式不是【用户自提】");
ErrorCode ORDER_UPDATE_ADDRESS_FAIL_STATUS_NOT_DELIVERED = new ErrorCode(1_011_000_031, "交易订单修改收货地址失败,原因:订单不是【待发货】状态");
ErrorCode ORDER_CREATE_FAIL_EXIST_UNPAID = new ErrorCode(1_011_000_032, "交易订单创建失败,原因:存在未付款订单");
+ ErrorCode ORDER_CANCEL_PAID_FAIL = new ErrorCode(1_011_000_033, "交易订单取消支付失败,原因:订单不是【{}】状态");
// ========== After Sale 模块 1-011-000-100 ==========
ErrorCode AFTER_SALE_NOT_FOUND = new ErrorCode(1_011_000_100, "售后单不存在");
@@ -59,6 +60,8 @@ public interface ErrorCodeConstants {
ErrorCode PRICE_CALCULATE_DELIVERY_PRICE_TEMPLATE_NOT_FOUND = new ErrorCode(1_011_003_002, "计算快递运费异常,找不到对应的运费模板");
ErrorCode PRICE_CALCULATE_COUPON_NOT_MATCH_NORMAL_ORDER = new ErrorCode(1_011_003_004, "参与秒杀、拼团、砍价的营销商品,无法使用优惠劵");
ErrorCode PRICE_CALCULATE_SECKILL_TOTAL_LIMIT_COUNT = new ErrorCode(1_011_003_005, "参与秒杀的商品,超过了秒杀总限购数量");
+ ErrorCode PRICE_CALCULATE_DELIVERY_PRICE_TYPE_ILLEGAL = new ErrorCode(1_011_003_006, "计算快递运费异常,配送方式不匹配");
+ ErrorCode PRICE_CALCULATE_COUPON_PRICE_TOO_MUCH = new ErrorCode(1_011_003_007, "该优惠劵无法使用,原因:优惠金额超过订单金额");
// ========== 物流 Express 模块 1-011-004-000 ==========
ErrorCode EXPRESS_NOT_EXISTS = new ErrorCode(1_011_004_000, "快递公司不存在");
diff --git a/yudao-module-mall/yudao-module-trade-api/src/main/java/cn/iocoder/yudao/module/trade/enums/order/TradeOrderCancelTypeEnum.java b/yudao-module-mall/yudao-module-trade-api/src/main/java/cn/iocoder/yudao/module/trade/enums/order/TradeOrderCancelTypeEnum.java
index 8ec1e9b16..cfd25468f 100644
--- a/yudao-module-mall/yudao-module-trade-api/src/main/java/cn/iocoder/yudao/module/trade/enums/order/TradeOrderCancelTypeEnum.java
+++ b/yudao-module-mall/yudao-module-trade-api/src/main/java/cn/iocoder/yudao/module/trade/enums/order/TradeOrderCancelTypeEnum.java
@@ -17,7 +17,8 @@ public enum TradeOrderCancelTypeEnum implements IntArrayValuable {
PAY_TIMEOUT(10, "超时未支付"),
AFTER_SALE_CLOSE(20, "退款关闭"),
- MEMBER_CANCEL(30, "买家取消");
+ MEMBER_CANCEL(30, "买家取消"),
+ COMBINATION_CLOSE(40, "拼团关闭");
public static final int[] ARRAYS = Arrays.stream(values()).mapToInt(TradeOrderCancelTypeEnum::getType).toArray();
diff --git a/yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/api/order/TradeOrderApiImpl.java b/yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/api/order/TradeOrderApiImpl.java
index 7426585d9..5e50f43ab 100644
--- a/yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/api/order/TradeOrderApiImpl.java
+++ b/yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/api/order/TradeOrderApiImpl.java
@@ -4,10 +4,10 @@ import cn.iocoder.yudao.module.trade.api.order.dto.TradeOrderRespDTO;
import cn.iocoder.yudao.module.trade.convert.order.TradeOrderConvert;
import cn.iocoder.yudao.module.trade.service.order.TradeOrderQueryService;
import cn.iocoder.yudao.module.trade.service.order.TradeOrderUpdateService;
+import jakarta.annotation.Resource;
import org.springframework.stereotype.Service;
import org.springframework.validation.annotation.Validated;
-import jakarta.annotation.Resource;
import java.util.Collection;
import java.util.List;
@@ -36,8 +36,8 @@ public class TradeOrderApiImpl implements TradeOrderApi {
}
@Override
- public void cancelPaidOrder(Long userId, Long orderId) {
- tradeOrderUpdateService.cancelPaidOrder(userId, orderId);
+ public void cancelPaidOrder(Long userId, Long orderId, Integer cancelType) {
+ tradeOrderUpdateService.cancelPaidOrder(userId, orderId, cancelType);
}
}
diff --git a/yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/dal/dataobject/order/TradeOrderDO.java b/yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/dal/dataobject/order/TradeOrderDO.java
index b127004aa..399b692ed 100644
--- a/yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/dal/dataobject/order/TradeOrderDO.java
+++ b/yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/dal/dataobject/order/TradeOrderDO.java
@@ -2,6 +2,7 @@ package cn.iocoder.yudao.module.trade.dal.dataobject.order;
import cn.iocoder.yudao.framework.common.enums.TerminalEnum;
import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO;
+import cn.iocoder.yudao.framework.mybatis.core.type.LongListTypeHandler;
import cn.iocoder.yudao.module.member.api.user.dto.MemberUserRespDTO;
import cn.iocoder.yudao.module.trade.dal.dataobject.brokerage.BrokerageUserDO;
import cn.iocoder.yudao.module.trade.dal.dataobject.delivery.DeliveryExpressDO;
@@ -12,10 +13,14 @@ import cn.iocoder.yudao.module.trade.enums.order.TradeOrderRefundStatusEnum;
import cn.iocoder.yudao.module.trade.enums.order.TradeOrderStatusEnum;
import cn.iocoder.yudao.module.trade.enums.order.TradeOrderTypeEnum;
import com.baomidou.mybatisplus.annotation.KeySequence;
+import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.annotation.TableName;
+import com.baomidou.mybatisplus.extension.handlers.JacksonTypeHandler;
import lombok.*;
import java.time.LocalDateTime;
+import java.util.List;
+import java.util.Map;
/**
* 交易订单 DO
@@ -291,6 +296,24 @@ public class TradeOrderDO extends BaseDO {
*/
private Integer vipPrice;
+ /**
+ * 赠送的优惠劵
+ *
+ * key: 优惠劵模版编号
+ * value:对应的优惠券数量
+ *
+ * 目的:用于订单支付后赠送优惠券
+ */
+ @TableField(typeHandler = JacksonTypeHandler.class)
+ private Map giveCouponTemplateCounts;
+ /**
+ * 赠送的优惠劵编号
+ *
+ * 目的:用于后续取消或者售后订单时,需要扣减赠送
+ */
+ @TableField(typeHandler = LongListTypeHandler.class)
+ private List giveCouponIds;
+
/**
* 秒杀活动编号
*
diff --git a/yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/service/brokerage/BrokerageUserServiceImpl.java b/yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/service/brokerage/BrokerageUserServiceImpl.java
index cf56a5bce..751151fe8 100644
--- a/yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/service/brokerage/BrokerageUserServiceImpl.java
+++ b/yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/service/brokerage/BrokerageUserServiceImpl.java
@@ -268,11 +268,6 @@ public class BrokerageUserServiceImpl implements BrokerageUserService {
return false;
}
- // 校验分佣模式:仅可后台手动设置推广员
- // if (BrokerageEnabledConditionEnum.ADMIN.getCondition().equals(tradeConfig.getBrokerageEnabledCondition())) {
- // throw exception(BROKERAGE_BIND_CONDITION_ADMIN);
- // }
-
// 校验分销关系绑定模式
if (BrokerageBindModeEnum.REGISTER.getMode().equals(tradeConfig.getBrokerageBindMode())) {
// 判断是否为新用户:注册时间在 30 秒内的,都算新用户
diff --git a/yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/service/order/TradeOrderUpdateService.java b/yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/service/order/TradeOrderUpdateService.java
index e16a08bd7..56b7cbc56 100644
--- a/yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/service/order/TradeOrderUpdateService.java
+++ b/yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/service/order/TradeOrderUpdateService.java
@@ -1,6 +1,5 @@
package cn.iocoder.yudao.module.trade.service.order;
-import cn.iocoder.yudao.framework.common.enums.TerminalEnum;
import cn.iocoder.yudao.module.trade.controller.admin.order.vo.TradeOrderDeliveryReqVO;
import cn.iocoder.yudao.module.trade.controller.admin.order.vo.TradeOrderRemarkReqVO;
import cn.iocoder.yudao.module.trade.controller.admin.order.vo.TradeOrderUpdateAddressReqVO;
@@ -10,9 +9,10 @@ import cn.iocoder.yudao.module.trade.controller.app.order.vo.AppTradeOrderSettle
import cn.iocoder.yudao.module.trade.controller.app.order.vo.AppTradeOrderSettlementRespVO;
import cn.iocoder.yudao.module.trade.controller.app.order.vo.item.AppTradeOrderItemCommentCreateReqVO;
import cn.iocoder.yudao.module.trade.dal.dataobject.order.TradeOrderDO;
-
import jakarta.validation.constraints.NotNull;
+import java.util.List;
+
/**
* 交易订单【写】Service 接口
*
@@ -187,13 +187,22 @@ public interface TradeOrderUpdateService {
*/
void updateOrderCombinationInfo(Long orderId, Long activityId, Long combinationRecordId, Long headId);
- // TODO 芋艿:拼团取消,不调这个接口哈;
/**
* 取消支付订单
*
- * @param userId 用户编号
- * @param orderId 订单编号
+ * @param userId 用户编号
+ * @param orderId 订单编号
+ * @param cancelType 取消类型
*/
- void cancelPaidOrder(Long userId, Long orderId);
+ void cancelPaidOrder(Long userId, Long orderId, Integer cancelType);
+
+ /**
+ * 更新下单赠送的优惠券编号到订单
+ *
+ * @param userId 用户编号
+ * @param orderId 订单编号
+ * @param giveCouponIds 赠送的优惠券编号列表
+ */
+ void updateOrderGiveCouponIds(Long userId, Long orderId, List giveCouponIds);
}
diff --git a/yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/service/order/TradeOrderUpdateServiceImpl.java b/yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/service/order/TradeOrderUpdateServiceImpl.java
index 945e36fc5..ce0c953e1 100644
--- a/yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/service/order/TradeOrderUpdateServiceImpl.java
+++ b/yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/service/order/TradeOrderUpdateServiceImpl.java
@@ -18,6 +18,8 @@ import cn.iocoder.yudao.module.member.api.address.dto.MemberAddressRespDTO;
import cn.iocoder.yudao.module.pay.api.order.PayOrderApi;
import cn.iocoder.yudao.module.pay.api.order.dto.PayOrderCreateReqDTO;
import cn.iocoder.yudao.module.pay.api.order.dto.PayOrderRespDTO;
+import cn.iocoder.yudao.module.pay.api.refund.PayRefundApi;
+import cn.iocoder.yudao.module.pay.api.refund.dto.PayRefundCreateReqDTO;
import cn.iocoder.yudao.module.pay.enums.order.PayOrderStatusEnum;
import cn.iocoder.yudao.module.product.api.comment.ProductCommentApi;
import cn.iocoder.yudao.module.product.api.comment.dto.ProductCommentCreateReqDTO;
@@ -111,6 +113,8 @@ public class TradeOrderUpdateServiceImpl implements TradeOrderUpdateService {
private ProductCommentApi productCommentApi;
@Resource
public SocialClientApi socialClientApi;
+ @Resource
+ public PayRefundApi payRefundApi;
@Resource
private TradeOrderProperties tradeOrderProperties;
@@ -197,6 +201,8 @@ public class TradeOrderUpdateServiceImpl implements TradeOrderUpdateService {
order.setRefundStatus(TradeOrderRefundStatusEnum.NONE.getStatus());
order.setProductCount(getSumValue(calculateRespBO.getItems(), TradePriceCalculateRespBO.OrderItem::getCount, Integer::sum));
order.setUserIp(getClientIP()).setTerminal(getTerminal());
+ // 使用 + 赠送优惠券
+ order.setGiveCouponTemplateCounts(calculateRespBO.getGiveCouponTemplateCounts());
// 支付 + 退款信息
order.setAdjustPrice(0).setPayStatus(false);
order.setRefundStatus(TradeOrderRefundStatusEnum.NONE.getStatus()).setRefundPrice(0);
@@ -854,15 +860,46 @@ public class TradeOrderUpdateServiceImpl implements TradeOrderUpdateService {
@Override
@Transactional(rollbackFor = Exception.class)
- public void cancelPaidOrder(Long userId, Long orderId) {
- // TODO @puhui999:需要校验状态;已支付的情况下,才可以。
+ public void cancelPaidOrder(Long userId, Long orderId, Integer cancelType) {
+ // 1.1 这里校验下 cancelType 只允许拼团关闭;
+ if (ObjUtil.notEqual(TradeOrderCancelTypeEnum.COMBINATION_CLOSE.getType(), cancelType)) {
+ return;
+ }
+ // 1.2 检验订单存在
TradeOrderDO order = tradeOrderMapper.selectOrderByIdAndUserId(orderId, userId);
if (order == null) {
throw exception(ORDER_NOT_FOUND);
}
- cancelOrder0(order, TradeOrderCancelTypeEnum.MEMBER_CANCEL);
- // TODO @puhui999:需要退款
+ // 1.3 校验订单是否支付
+ if (!order.getPayStatus()) {
+ throw exception(ORDER_CANCEL_PAID_FAIL, "已支付");
+ }
+ // 1.3 校验订单是否已退款
+ if (ObjUtil.equal(TradeOrderRefundStatusEnum.NONE.getStatus(), order.getRefundStatus())) {
+ throw exception(ORDER_CANCEL_PAID_FAIL, "未退款");
+ }
+
+ // 2.1 取消订单
+ cancelOrder0(order, TradeOrderCancelTypeEnum.COMBINATION_CLOSE);
+ // 2.2 创建退款单
+ payRefundApi.createRefund(new PayRefundCreateReqDTO()
+ .setAppKey(tradeOrderProperties.getPayAppKey()).setUserIp(getClientIP()) // 支付应用
+ .setMerchantOrderId(String.valueOf(order.getId())) // 支付单号
+ .setMerchantRefundId(String.valueOf(order.getId()))
+ .setReason(TradeOrderCancelTypeEnum.COMBINATION_CLOSE.getName()).setPrice(order.getPayPrice()));// 价格信息
+ }
+
+ @Override
+ public void updateOrderGiveCouponIds(Long userId, Long orderId, List giveCouponIds) {
+ // 1. 检验订单存在
+ TradeOrderDO order = tradeOrderMapper.selectOrderByIdAndUserId(orderId, userId);
+ if (order == null) {
+ throw exception(ORDER_NOT_FOUND);
+ }
+
+ // 2. 更新订单赠送的优惠券编号列表
+ tradeOrderMapper.updateById(new TradeOrderDO().setId(orderId).setGiveCouponIds(giveCouponIds));
}
/**
diff --git a/yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/service/order/handler/TradeCouponOrderHandler.java b/yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/service/order/handler/TradeCouponOrderHandler.java
index 478de450f..f5d7da4d4 100644
--- a/yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/service/order/handler/TradeCouponOrderHandler.java
+++ b/yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/service/order/handler/TradeCouponOrderHandler.java
@@ -1,12 +1,16 @@
package cn.iocoder.yudao.module.trade.service.order.handler;
+import cn.hutool.core.collection.CollUtil;
import cn.iocoder.yudao.module.promotion.api.coupon.CouponApi;
import cn.iocoder.yudao.module.promotion.api.coupon.dto.CouponUseReqDTO;
import cn.iocoder.yudao.module.trade.dal.dataobject.order.TradeOrderDO;
import cn.iocoder.yudao.module.trade.dal.dataobject.order.TradeOrderItemDO;
+import cn.iocoder.yudao.module.trade.service.order.TradeOrderQueryService;
+import cn.iocoder.yudao.module.trade.service.order.TradeOrderUpdateService;
+import jakarta.annotation.Resource;
+import org.springframework.context.annotation.Lazy;
import org.springframework.stereotype.Component;
-import jakarta.annotation.Resource;
import java.util.List;
/**
@@ -17,6 +21,12 @@ import java.util.List;
@Component
public class TradeCouponOrderHandler implements TradeOrderHandler {
+ @Resource
+ @Lazy // 延迟加载,避免循环依赖
+ private TradeOrderUpdateService orderUpdateService;
+ @Resource
+ private TradeOrderQueryService orderQueryService;
+
@Resource
private CouponApi couponApi;
@@ -31,12 +41,30 @@ public class TradeCouponOrderHandler implements TradeOrderHandler {
}
@Override
- public void afterCancelOrder(TradeOrderDO order, List orderItems) {
- if (order.getCouponId() == null || order.getCouponId() <= 0) {
+ public void afterPayOrder(TradeOrderDO order, List orderItems) {
+ if (CollUtil.isEmpty(order.getGiveCouponTemplateCounts())) {
return;
}
- // 退回优惠劵
- couponApi.returnUsedCoupon(order.getCouponId());
+ // 赠送优惠券
+ List couponIds = couponApi.takeCouponsByAdmin(order.getGiveCouponTemplateCounts(), order.getUserId());
+ if (CollUtil.isEmpty(couponIds)) {
+ return;
+ }
+ orderUpdateService.updateOrderGiveCouponIds(order.getUserId(), order.getId(), couponIds);
+ }
+
+ @Override
+ public void afterCancelOrder(TradeOrderDO order, List orderItems) {
+ // 情况一:退还订单使用的优惠券
+ if (order.getCouponId() != null && order.getCouponId() > 0) {
+ // 退回优惠劵
+ couponApi.returnUsedCoupon(order.getCouponId());
+ }
+ // 情况二:收回赠送的优惠券
+ if (CollUtil.isEmpty(order.getGiveCouponIds())) {
+ return;
+ }
+ couponApi.invalidateCouponsByAdmin(order.getGiveCouponIds(), order.getUserId());
}
}
diff --git a/yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/service/price/bo/TradePriceCalculateRespBO.java b/yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/service/price/bo/TradePriceCalculateRespBO.java
index b7482407c..4f65f33d1 100644
--- a/yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/service/price/bo/TradePriceCalculateRespBO.java
+++ b/yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/service/price/bo/TradePriceCalculateRespBO.java
@@ -6,6 +6,7 @@ import cn.iocoder.yudao.module.trade.enums.order.TradeOrderTypeEnum;
import lombok.Data;
import java.util.List;
+import java.util.Map;
/**
* 价格计算 Response BO
@@ -67,6 +68,21 @@ public class TradePriceCalculateRespBO {
*/
private Long bargainActivityId;
+ /**
+ * 是否包邮
+ */
+ private Boolean freeDelivery;
+
+ /**
+ * 赠送的优惠劵
+ *
+ * key: 优惠劵模版编号
+ * value:对应的优惠券数量
+ *
+ * 目的:用于订单支付后赠送优惠券
+ */
+ private Map giveCouponTemplateCounts;
+
/**
* 订单价格
*/
@@ -213,8 +229,19 @@ public class TradePriceCalculateRespBO {
*/
private Long categoryId;
+ // ========== 物流相关字段 =========
+
/**
- * 运费模板 Id
+ * 配送方式数组
+ *
+ * 对应 DeliveryTypeEnum 枚举
+ */
+ private List deliveryTypes;
+
+ /**
+ * 物流配置模板编号
+ *
+ * 对应 TradeDeliveryExpressTemplateDO 的 id 编号
*/
private Long deliveryTemplateId;
@@ -234,7 +261,7 @@ public class TradePriceCalculateRespBO {
private List properties;
/**
- * 使用的积分
+ * 赠送的积分
*/
private Integer givePoint;
diff --git a/yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/service/price/calculator/TradeCouponPriceCalculator.java b/yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/service/price/calculator/TradeCouponPriceCalculator.java
index 3bdfe509f..1c7294be5 100644
--- a/yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/service/price/calculator/TradeCouponPriceCalculator.java
+++ b/yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/service/price/calculator/TradeCouponPriceCalculator.java
@@ -25,6 +25,7 @@ import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.
import static cn.iocoder.yudao.module.promotion.enums.ErrorCodeConstants.COUPON_NO_MATCH_MIN_PRICE;
import static cn.iocoder.yudao.module.promotion.enums.ErrorCodeConstants.COUPON_NO_MATCH_SPU;
import static cn.iocoder.yudao.module.trade.enums.ErrorCodeConstants.PRICE_CALCULATE_COUPON_NOT_MATCH_NORMAL_ORDER;
+import static cn.iocoder.yudao.module.trade.enums.ErrorCodeConstants.PRICE_CALCULATE_COUPON_PRICE_TOO_MUCH;
/**
* 优惠劵的 {@link TradePriceCalculator} 实现类
@@ -65,8 +66,9 @@ public class TradeCouponPriceCalculator implements TradePriceCalculator {
// 3.1 计算可以优惠的金额
Integer couponPrice = getCouponPrice(coupon, totalPayPrice);
- Assert.isTrue(couponPrice < totalPayPrice,
- "优惠劵({}) 的优惠金额({}),不能大于订单总金额({})", coupon.getId(), couponPrice, totalPayPrice);
+ if (couponPrice <= totalPayPrice) {
+ throw exception(PRICE_CALCULATE_COUPON_PRICE_TOO_MUCH);
+ }
// 3.2 计算分摊的优惠金额
List divideCouponPrices = TradePriceCalculatorHelper.dividePrice(orderItems, couponPrice);
diff --git a/yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/service/price/calculator/TradeDeliveryPriceCalculator.java b/yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/service/price/calculator/TradeDeliveryPriceCalculator.java
index d9fed7aeb..8c0829f9a 100644
--- a/yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/service/price/calculator/TradeDeliveryPriceCalculator.java
+++ b/yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/service/price/calculator/TradeDeliveryPriceCalculator.java
@@ -4,6 +4,7 @@ import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.lang.Assert;
import cn.hutool.core.util.StrUtil;
import cn.iocoder.yudao.framework.common.enums.CommonStatusEnum;
+import cn.iocoder.yudao.framework.common.util.collection.CollectionUtils;
import cn.iocoder.yudao.module.member.api.address.MemberAddressApi;
import cn.iocoder.yudao.module.member.api.address.dto.MemberAddressRespDTO;
import cn.iocoder.yudao.module.trade.dal.dataobject.config.TradeConfigDO;
@@ -17,11 +18,11 @@ import cn.iocoder.yudao.module.trade.service.delivery.bo.DeliveryExpressTemplate
import cn.iocoder.yudao.module.trade.service.price.bo.TradePriceCalculateReqBO;
import cn.iocoder.yudao.module.trade.service.price.bo.TradePriceCalculateRespBO;
import cn.iocoder.yudao.module.trade.service.price.bo.TradePriceCalculateRespBO.OrderItem;
+import jakarta.annotation.Resource;
import lombok.extern.slf4j.Slf4j;
import org.springframework.core.annotation.Order;
import org.springframework.stereotype.Component;
-import jakarta.annotation.Resource;
import java.util.List;
import java.util.Map;
import java.util.Set;
@@ -55,7 +56,11 @@ public class TradeDeliveryPriceCalculator implements TradePriceCalculator {
if (param.getDeliveryType() == null) {
return;
}
- // TODO @puhui999:需要校验,是不是存在商品不能门店自提,或者不能快递发货的情况。就是说,配送方式不匹配哈
+ // 校验是不是存在商品不能门店自提,或者不能快递发货的情况。就是说,配送方式不匹配哈
+ if (CollectionUtils.anyMatch(result.getItems(), item -> !item.getDeliveryTypes().contains(param.getDeliveryType()))) {
+ throw exception(PRICE_CALCULATE_DELIVERY_PRICE_TYPE_ILLEGAL);
+ }
+
if (DeliveryTypeEnum.PICK_UP.getType().equals(param.getDeliveryType())) {
calculateByPickUp(param);
} else if (DeliveryTypeEnum.EXPRESS.getType().equals(param.getDeliveryType())) {
@@ -90,7 +95,12 @@ public class TradeDeliveryPriceCalculator implements TradePriceCalculator {
return;
}
- // 情况二:快递模版
+ // 情况二:活动包邮
+ if (Boolean.TRUE.equals(result.getFreeDelivery())) {
+ return;
+ }
+
+ // 情况三:快递模版
// 2.1 过滤出已选中的商品 SKU
List selectedItem = filterList(result.getItems(), OrderItem::getSelected);
Set deliveryTemplateIds = convertSet(selectedItem, OrderItem::getDeliveryTemplateId);
@@ -124,7 +134,7 @@ public class TradeDeliveryPriceCalculator implements TradePriceCalculator {
Map> template2ItemMap = convertMultiMap(selectedSkus, OrderItem::getDeliveryTemplateId);
// 依次计算快递运费
for (Map.Entry> entry : template2ItemMap.entrySet()) {
- Long templateId = entry.getKey();
+ Long templateId = entry.getKey();
List orderItems = entry.getValue();
DeliveryExpressTemplateRespBO templateBO = expressTemplateMap.get(templateId);
if (templateBO == null) {
@@ -144,8 +154,8 @@ public class TradeDeliveryPriceCalculator implements TradePriceCalculator {
/**
* 按配送方式来计算运费
*
- * @param orderItems SKU 商品项目
- * @param chargeMode 配送计费方式
+ * @param orderItems SKU 商品项目
+ * @param chargeMode 配送计费方式
* @param templateCharge 快递运费配置
*/
private void calculateExpressFeeByChargeMode(List orderItems, Integer chargeMode,
diff --git a/yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/service/price/calculator/TradePriceCalculatorHelper.java b/yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/service/price/calculator/TradePriceCalculatorHelper.java
index 891f1e0dc..323b50e93 100644
--- a/yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/service/price/calculator/TradePriceCalculatorHelper.java
+++ b/yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/service/price/calculator/TradePriceCalculatorHelper.java
@@ -11,6 +11,7 @@ import cn.iocoder.yudao.module.trade.service.price.bo.TradePriceCalculateReqBO;
import cn.iocoder.yudao.module.trade.service.price.bo.TradePriceCalculateRespBO;
import java.util.ArrayList;
+import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
@@ -31,8 +32,7 @@ public class TradePriceCalculatorHelper {
List spuList, List skuList) {
// 创建 PriceCalculateRespDTO 对象
TradePriceCalculateRespBO result = new TradePriceCalculateRespBO();
- result.setType(getOrderType(param));
- result.setPromotions(new ArrayList<>());
+ result.setType(getOrderType(param)).setPromotions(new ArrayList<>()).setGiveCouponTemplateCounts(new LinkedHashMap<>());
// 创建它的 OrderItem 属性
result.setItems(new ArrayList<>(param.getItems().size()));
@@ -60,7 +60,7 @@ public class TradePriceCalculatorHelper {
.setWeight(sku.getWeight()).setVolume(sku.getVolume());
// spu 信息
orderItem.setSpuName(spu.getName()).setCategoryId(spu.getCategoryId())
- .setDeliveryTemplateId(spu.getDeliveryTemplateId())
+ .setDeliveryTypes(spu.getDeliveryTypes()).setDeliveryTemplateId(spu.getDeliveryTemplateId())
.setGivePoint(spu.getGiveIntegral()).setUsePoint(0);
if (StrUtil.isBlank(orderItem.getPicUrl())) {
orderItem.setPicUrl(spu.getPicUrl());
diff --git a/yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/service/price/calculator/TradeRewardActivityPriceCalculator.java b/yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/service/price/calculator/TradeRewardActivityPriceCalculator.java
index 4374783d2..50d424c29 100644
--- a/yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/service/price/calculator/TradeRewardActivityPriceCalculator.java
+++ b/yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/service/price/calculator/TradeRewardActivityPriceCalculator.java
@@ -3,23 +3,30 @@ package cn.iocoder.yudao.module.trade.service.price.calculator;
import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.util.ObjectUtil;
import cn.hutool.core.util.StrUtil;
+import cn.iocoder.yudao.framework.common.util.number.MoneyUtils;
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.PromotionProductScopeEnum;
import cn.iocoder.yudao.module.promotion.enums.common.PromotionTypeEnum;
import cn.iocoder.yudao.module.trade.enums.order.TradeOrderTypeEnum;
import cn.iocoder.yudao.module.trade.service.price.bo.TradePriceCalculateReqBO;
import cn.iocoder.yudao.module.trade.service.price.bo.TradePriceCalculateRespBO;
+import jakarta.annotation.Resource;
import org.springframework.core.annotation.Order;
import org.springframework.stereotype.Component;
-import jakarta.annotation.Resource;
+import java.util.ArrayList;
+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;
+// TODO @puhui999:相关的单测,建议改一改
+
/**
* 满减送活动的 {@link TradePriceCalculator} 实现类
*
@@ -52,7 +59,7 @@ public class TradeRewardActivityPriceCalculator implements TradePriceCalculator
private void calculate(TradePriceCalculateReqBO param, TradePriceCalculateRespBO result,
RewardActivityMatchRespDTO rewardActivity) {
// 1.1 获得满减送的订单项(商品)列表
- List orderItems = filterMatchCouponOrderItems(result, rewardActivity);
+ List orderItems = filterMatchActivityOrderItems(result, rewardActivity);
if (CollUtil.isEmpty(orderItems)) {
return;
}
@@ -61,7 +68,7 @@ public class TradeRewardActivityPriceCalculator implements TradePriceCalculator
if (rule == null) {
TradePriceCalculatorHelper.addNotMatchPromotion(result, orderItems,
rewardActivity.getId(), rewardActivity.getName(), PromotionTypeEnum.REWARD_ACTIVITY.getType(),
- getRewardActivityNotMeetTip(rewardActivity));
+ getRewardActivityNotMeetTip(rewardActivity, orderItems));
return;
}
@@ -84,6 +91,36 @@ public class TradeRewardActivityPriceCalculator implements TradePriceCalculator
TradePriceCalculatorHelper.recountPayPrice(orderItem);
}
TradePriceCalculatorHelper.recountAllPrice(result);
+
+ // 4.1 记录赠送的积分
+ if (rule.getPoint() != null && rule.getPoint() > 0) {
+ List dividePoints = TradePriceCalculatorHelper.dividePrice(orderItems, rule.getPoint());
+ for (int i = 0; i < orderItems.size(); i++) {
+ // 商品可能赠送了积分,所以这里要加上
+ TradePriceCalculateRespBO.OrderItem orderItem = orderItems.get(i);
+ orderItem.setGivePoint(orderItem.getGivePoint() + dividePoints.get(i));
+ }
+ }
+ // 4.2 记录订单是否包邮
+ if (Boolean.TRUE.equals(rule.getFreeDelivery())) {
+ // 只要满足一个活动包邮那么这单就包邮
+ result.setFreeDelivery(true);
+ }
+ // 4.3 记录赠送的优惠券
+ if (CollUtil.isNotEmpty(rule.getGiveCouponTemplateCounts())) {
+ for (Map.Entry entry : rule.getGiveCouponTemplateCounts().entrySet()) {
+ Map giveCouponTemplateCounts = result.getGiveCouponTemplateCounts();
+ // TODO @puhui999:是不是有一种可能性,这个 key 没有,别的 key 有哈。
+ // TODO 这里还有一种简化的写法。就是下面,大概两行就可以啦
+// result.getGiveCouponTemplateCounts().put(entry.getKey(),
+// result.getGiveCouponTemplateCounts().getOrDefault(entry.getKey(), 0) + entry.getValue());
+ if (giveCouponTemplateCounts.get(entry.getKey()) == null) { // 情况一:还没有赠送的优惠券
+ result.setGiveCouponTemplateCounts(rule.getGiveCouponTemplateCounts());
+ } else { // 情况二:别的满减活动送过同类优惠券,则直接增加数量
+ giveCouponTemplateCounts.put(entry.getKey(), giveCouponTemplateCounts.get(entry.getKey()) + entry.getValue());
+ }
+ }
+ }
}
/**
@@ -93,10 +130,23 @@ public class TradeRewardActivityPriceCalculator implements TradePriceCalculator
* @param rewardActivity 满减送活动
* @return 订单项(商品)列表
*/
- private List filterMatchCouponOrderItems(TradePriceCalculateRespBO result,
- RewardActivityMatchRespDTO rewardActivity) {
- return filterList(result.getItems(),
- orderItem -> CollUtil.contains(rewardActivity.getSpuIds(), orderItem.getSpuId()));
+ private List filterMatchActivityOrderItems(TradePriceCalculateRespBO result,
+ RewardActivityMatchRespDTO rewardActivity) {
+ // 情况一:全部商品都可以参与
+ if (PromotionProductScopeEnum.isAll(rewardActivity.getProductScope())) {
+ return result.getItems();
+ }
+ // 情况二:指定商品参与
+ if (PromotionProductScopeEnum.isSpu(rewardActivity.getProductScope())) {
+ return filterList(result.getItems(),
+ orderItem -> CollUtil.contains(rewardActivity.getProductScopeValues(), orderItem.getSpuId()));
+ }
+ // 情况三:指定商品类型参与
+ if (PromotionProductScopeEnum.isCategory(rewardActivity.getProductScope())) {
+ return filterList(result.getItems(),
+ orderItem -> CollUtil.contains(rewardActivity.getProductScopeValues(), orderItem.getCategoryId()));
+ }
+ return List.of();
}
/**
@@ -129,14 +179,30 @@ public class TradeRewardActivityPriceCalculator implements TradePriceCalculator
}
/**
- * 获得满减送活动部匹配时的提示
+ * 获得满减送活动不匹配时的提示
*
* @param rewardActivity 满减送活动
* @return 提示
*/
- private String getRewardActivityNotMeetTip(RewardActivityMatchRespDTO rewardActivity) {
- // TODO 芋艿:后面再想想;应该找第一个规则,算下还差多少即可。
- return "TODO";
+ private String getRewardActivityNotMeetTip(RewardActivityMatchRespDTO rewardActivity,
+ List orderItems) {
+ // 1. 计算数量和价格
+ Integer count = TradePriceCalculatorHelper.calculateTotalCount(orderItems);
+ Integer price = TradePriceCalculatorHelper.calculateTotalPayPrice(orderItems);
+ assert count != null && price != null;
+
+ // 2. 构建不满足时的提示信息:按最低档规则算
+ String meetTip = "满减送:购满 {} {},可以减 {} 元";
+ List rules = new ArrayList<>(rewardActivity.getRules());
+ rules.sort(Comparator.comparing(RewardActivityMatchRespDTO.Rule::getLimit)); // 按优惠门槛升序
+ RewardActivityMatchRespDTO.Rule rule = rules.get(0);
+ if (PromotionConditionTypeEnum.PRICE.getType().equals(rewardActivity.getConditionType())) {
+ return StrUtil.format(meetTip, rule.getLimit(), "元", MoneyUtils.fenToYuanStr(rule.getDiscountPrice()));
+ }
+ if (PromotionConditionTypeEnum.COUNT.getType().equals(rewardActivity.getConditionType())) {
+ return StrUtil.format(meetTip, rule.getLimit(), "件", MoneyUtils.fenToYuanStr(rule.getDiscountPrice()));
+ }
+ return StrUtil.EMPTY;
}
}
diff --git a/yudao-module-mall/yudao-module-trade-biz/src/test/java/cn/iocoder/yudao/module/trade/service/price/calculator/TradeRewardActivityPriceCalculatorTest.java b/yudao-module-mall/yudao-module-trade-biz/src/test/java/cn/iocoder/yudao/module/trade/service/price/calculator/TradeRewardActivityPriceCalculatorTest.java
index de72ed616..ba93fc10e 100644
--- a/yudao-module-mall/yudao-module-trade-biz/src/test/java/cn/iocoder/yudao/module/trade/service/price/calculator/TradeRewardActivityPriceCalculatorTest.java
+++ b/yudao-module-mall/yudao-module-trade-biz/src/test/java/cn/iocoder/yudao/module/trade/service/price/calculator/TradeRewardActivityPriceCalculatorTest.java
@@ -4,6 +4,7 @@ import cn.iocoder.yudao.framework.test.core.ut.BaseMockitoUnitTest;
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.PromotionProductScopeEnum;
import cn.iocoder.yudao.module.promotion.enums.common.PromotionTypeEnum;
import cn.iocoder.yudao.module.trade.enums.order.TradeOrderTypeEnum;
import cn.iocoder.yudao.module.trade.service.price.bo.TradePriceCalculateReqBO;
@@ -13,6 +14,7 @@ import org.mockito.InjectMocks;
import org.mockito.Mock;
import java.util.ArrayList;
+import java.util.LinkedHashMap;
import static cn.iocoder.yudao.framework.common.util.collection.SetUtils.asSet;
import static cn.iocoder.yudao.framework.test.core.util.RandomUtils.randomPojo;
@@ -47,7 +49,7 @@ public class TradeRewardActivityPriceCalculatorTest extends BaseMockitoUnitTest
TradePriceCalculateRespBO result = new TradePriceCalculateRespBO()
.setType(TradeOrderTypeEnum.NORMAL.getType())
.setPrice(new TradePriceCalculateRespBO.Price())
- .setPromotions(new ArrayList<>())
+ .setPromotions(new ArrayList<>()).setGiveCouponTemplateCounts(new LinkedHashMap<>())
.setItems(asList(
new TradePriceCalculateRespBO.OrderItem().setSkuId(10L).setCount(2).setSelected(true)
.setPrice(100).setSpuId(1L),
@@ -60,16 +62,22 @@ public class TradeRewardActivityPriceCalculatorTest extends BaseMockitoUnitTest
TradePriceCalculatorHelper.recountPayPrice(result.getItems());
TradePriceCalculatorHelper.recountAllPrice(result);
- // mock 方法(限时折扣 DiscountActivity 信息)
+ // mock 方法(满减送 RewardActivity 信息)
when(rewardActivityApi.getMatchRewardActivityList(eq(asSet(1L, 2L, 3L)))).thenReturn(asList(
randomPojo(RewardActivityMatchRespDTO.class, o -> o.setId(1000L).setName("活动 1000 号")
- .setSpuIds(asList(1L, 2L)).setConditionType(PromotionConditionTypeEnum.PRICE.getType())
- .setRules(singletonList(new RewardActivityMatchRespDTO.Rule().setLimit(200).setDiscountPrice(70)))),
+ .setConditionType(PromotionConditionTypeEnum.PRICE.getType())
+ .setProductScope(PromotionProductScopeEnum.SPU.getScope()).setProductScopeValues(asList(1L, 2L))
+ .setRules(singletonList(new RewardActivityMatchRespDTO.Rule().setLimit(20).setDiscountPrice(70)
+ .setFreeDelivery(false)))),
randomPojo(RewardActivityMatchRespDTO.class, o -> o.setId(2000L).setName("活动 2000 号")
- .setSpuIds(singletonList(3L)).setConditionType(PromotionConditionTypeEnum.COUNT.getType())
- .setRules(asList(new RewardActivityMatchRespDTO.Rule().setLimit(1).setDiscountPrice(10),
- new RewardActivityMatchRespDTO.Rule().setLimit(2).setDiscountPrice(60), // 最大可满足,因为是 4 个
- new RewardActivityMatchRespDTO.Rule().setLimit(10).setDiscountPrice(100))))
+ .setConditionType(PromotionConditionTypeEnum.COUNT.getType())
+ .setProductScope(PromotionProductScopeEnum.SPU.getScope()).setProductScopeValues(singletonList(3L))
+ .setRules(asList(new RewardActivityMatchRespDTO.Rule().setLimit(1).setDiscountPrice(10)
+ .setPoint(50).setFreeDelivery(false),
+ new RewardActivityMatchRespDTO.Rule().setLimit(2).setDiscountPrice(60)
+ .setPoint(100).setFreeDelivery(false), // 最大可满足,因为是 4 个
+ new RewardActivityMatchRespDTO.Rule().setLimit(10).setDiscountPrice(100)
+ .setFreeDelivery(false))))
));
// 调用
@@ -94,6 +102,7 @@ public class TradeRewardActivityPriceCalculatorTest extends BaseMockitoUnitTest
assertEquals(orderItem01.getCouponPrice(), 0);
assertEquals(orderItem01.getPointPrice(), 0);
assertEquals(orderItem01.getPayPrice(), 160);
+ assertEquals(orderItem01.getGivePoint(), 0);
// 断言:SKU 2
TradePriceCalculateRespBO.OrderItem orderItem02 = result.getItems().get(1);
assertEquals(orderItem02.getSkuId(), 20L);
@@ -104,6 +113,7 @@ public class TradeRewardActivityPriceCalculatorTest extends BaseMockitoUnitTest
assertEquals(orderItem02.getCouponPrice(), 0);
assertEquals(orderItem02.getPointPrice(), 0);
assertEquals(orderItem02.getPayPrice(), 120);
+ assertEquals(orderItem02.getGivePoint(), 0);
// 断言:SKU 3
TradePriceCalculateRespBO.OrderItem orderItem03 = result.getItems().get(2);
assertEquals(orderItem03.getSkuId(), 30L);
@@ -114,6 +124,7 @@ public class TradeRewardActivityPriceCalculatorTest extends BaseMockitoUnitTest
assertEquals(orderItem03.getCouponPrice(), 0);
assertEquals(orderItem03.getPointPrice(), 0);
assertEquals(orderItem03.getPayPrice(), 60);
+ assertEquals(orderItem03.getGivePoint(), 100);
// 断言:Promotion 部分(第一个)
assertEquals(result.getPromotions().size(), 2);
TradePriceCalculateRespBO.Promotion promotion01 = result.getPromotions().get(0);
@@ -175,7 +186,7 @@ public class TradeRewardActivityPriceCalculatorTest extends BaseMockitoUnitTest
// mock 方法(限时折扣 DiscountActivity 信息)
when(rewardActivityApi.getMatchRewardActivityList(eq(asSet(1L, 2L)))).thenReturn(singletonList(
randomPojo(RewardActivityMatchRespDTO.class, o -> o.setId(1000L).setName("活动 1000 号")
- .setSpuIds(asList(1L, 2L)).setConditionType(PromotionConditionTypeEnum.PRICE.getType())
+ .setProductScopeValues(asList(1L, 2L)).setConditionType(PromotionConditionTypeEnum.PRICE.getType())
.setRules(singletonList(new RewardActivityMatchRespDTO.Rule().setLimit(351).setDiscountPrice(70))))
));
diff --git a/yudao-module-mall/yudao-module-trade-biz/src/test/resources/sql/create_tables.sql b/yudao-module-mall/yudao-module-trade-biz/src/test/resources/sql/create_tables.sql
index f619c01de..1d7ed24ee 100644
--- a/yudao-module-mall/yudao-module-trade-biz/src/test/resources/sql/create_tables.sql
+++ b/yudao-module-mall/yudao-module-trade-biz/src/test/resources/sql/create_tables.sql
@@ -48,6 +48,7 @@ CREATE TABLE IF NOT EXISTS "trade_order"
"give_point" int NULL,
"refund_point" int NULL,
"vip_price" int NULL,
+ "give_coupons_map" varchar NULL,
"seckill_activity_id" long NULL,
"bargain_activity_id" long NULL,
"bargain_record_id" long NULL,
diff --git a/yudao-module-member/yudao-module-member-biz/src/main/java/cn/iocoder/yudao/module/member/controller/admin/user/MemberUserController.java b/yudao-module-member/yudao-module-member-biz/src/main/java/cn/iocoder/yudao/module/member/controller/admin/user/MemberUserController.java
index c9c6c06e0..6a642bbf0 100644
--- a/yudao-module-member/yudao-module-member-biz/src/main/java/cn/iocoder/yudao/module/member/controller/admin/user/MemberUserController.java
+++ b/yudao-module-member/yudao-module-member-biz/src/main/java/cn/iocoder/yudao/module/member/controller/admin/user/MemberUserController.java
@@ -18,12 +18,12 @@ import cn.iocoder.yudao.module.member.service.user.MemberUserService;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.tags.Tag;
+import jakarta.annotation.Resource;
+import jakarta.validation.Valid;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.*;
-import jakarta.annotation.Resource;
-import jakarta.validation.Valid;
import java.util.Collection;
import java.util.List;
import java.util.Objects;
@@ -76,14 +76,6 @@ public class MemberUserController {
return success(true);
}
- @PutMapping("/update-balance")
- @Operation(summary = "更新会员用户余额")
- @PreAuthorize("@ss.hasPermission('member:user:update-balance')")
- public CommonResult updateUserBalance(@Valid @RequestBody Long id) {
- // todo @jason:增加一个【修改余额】
- return success(true);
- }
-
@GetMapping("/get")
@Operation(summary = "获得会员用户")
@Parameter(name = "id", description = "编号", required = true, example = "1024")
diff --git a/yudao-module-pay/yudao-module-pay-api/src/main/java/cn/iocoder/yudao/module/pay/enums/wallet/PayWalletBizTypeEnum.java b/yudao-module-pay/yudao-module-pay-api/src/main/java/cn/iocoder/yudao/module/pay/enums/wallet/PayWalletBizTypeEnum.java
index 20e0a8b09..ae99128b9 100644
--- a/yudao-module-pay/yudao-module-pay-api/src/main/java/cn/iocoder/yudao/module/pay/enums/wallet/PayWalletBizTypeEnum.java
+++ b/yudao-module-pay/yudao-module-pay-api/src/main/java/cn/iocoder/yudao/module/pay/enums/wallet/PayWalletBizTypeEnum.java
@@ -18,9 +18,8 @@ public enum PayWalletBizTypeEnum implements IntArrayValuable {
RECHARGE(1, "充值"),
RECHARGE_REFUND(2, "充值退款"),
PAYMENT(3, "支付"),
- PAYMENT_REFUND(4, "支付退款");
-
- // TODO 后续增加
+ PAYMENT_REFUND(4, "支付退款"),
+ UPDATE_BALANCE(5, "更新余额");
/**
* 业务分类
diff --git a/yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/controller/admin/wallet/PayWalletController.java b/yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/controller/admin/wallet/PayWalletController.java
index 15e381538..54fa00419 100644
--- a/yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/controller/admin/wallet/PayWalletController.java
+++ b/yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/controller/admin/wallet/PayWalletController.java
@@ -4,9 +4,11 @@ import cn.iocoder.yudao.framework.common.pojo.CommonResult;
import cn.iocoder.yudao.framework.common.pojo.PageResult;
import cn.iocoder.yudao.module.pay.controller.admin.wallet.vo.wallet.PayWalletPageReqVO;
import cn.iocoder.yudao.module.pay.controller.admin.wallet.vo.wallet.PayWalletRespVO;
+import cn.iocoder.yudao.module.pay.controller.admin.wallet.vo.wallet.PayWalletUpdateBalanceReqVO;
import cn.iocoder.yudao.module.pay.controller.admin.wallet.vo.wallet.PayWalletUserReqVO;
import cn.iocoder.yudao.module.pay.convert.wallet.PayWalletConvert;
import cn.iocoder.yudao.module.pay.dal.dataobject.wallet.PayWalletDO;
+import cn.iocoder.yudao.module.pay.enums.wallet.PayWalletBizTypeEnum;
import cn.iocoder.yudao.module.pay.service.wallet.PayWalletService;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
@@ -15,12 +17,12 @@ import jakarta.validation.Valid;
import lombok.extern.slf4j.Slf4j;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.validation.annotation.Validated;
-import org.springframework.web.bind.annotation.GetMapping;
-import org.springframework.web.bind.annotation.RequestMapping;
-import org.springframework.web.bind.annotation.RestController;
+import org.springframework.web.bind.annotation.*;
import static cn.iocoder.yudao.framework.common.enums.UserTypeEnum.MEMBER;
+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.module.pay.enums.ErrorCodeConstants.WALLET_NOT_FOUND;
@Tag(name = "管理后台 - 用户钱包")
@RestController
@@ -48,4 +50,21 @@ public class PayWalletController {
return success(PayWalletConvert.INSTANCE.convertPage(pageResult));
}
+ @PutMapping("/update-balance")
+ @Operation(summary = "更新会员用户余额")
+ @PreAuthorize("@ss.hasPermission('pay:wallet:update-balance')")
+ public CommonResult updateWalletBalance(@Valid @RequestBody PayWalletUpdateBalanceReqVO updateReqVO) {
+ // 获得用户钱包
+ PayWalletDO wallet = payWalletService.getOrCreateWallet(updateReqVO.getUserId(), MEMBER.getValue());
+ if (wallet == null) {
+ log.error("[updateWalletBalance],updateReqVO({}) 用户钱包不存在.", updateReqVO);
+ throw exception(WALLET_NOT_FOUND);
+ }
+
+ // 更新钱包余额
+ payWalletService.addWalletBalance(wallet.getId(), String.valueOf(updateReqVO.getUserId()),
+ PayWalletBizTypeEnum.UPDATE_BALANCE, updateReqVO.getBalance());
+ return success(true);
+ }
+
}
diff --git a/yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/controller/admin/wallet/vo/wallet/PayWalletUpdateBalanceReqVO.java b/yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/controller/admin/wallet/vo/wallet/PayWalletUpdateBalanceReqVO.java
new file mode 100644
index 000000000..7569bca78
--- /dev/null
+++ b/yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/controller/admin/wallet/vo/wallet/PayWalletUpdateBalanceReqVO.java
@@ -0,0 +1,19 @@
+package cn.iocoder.yudao.module.pay.controller.admin.wallet.vo.wallet;
+
+import io.swagger.v3.oas.annotations.media.Schema;
+import jakarta.validation.constraints.NotNull;
+import lombok.Data;
+
+@Schema(description = "管理后台 - 修改钱包余额 Request VO")
+@Data
+public class PayWalletUpdateBalanceReqVO {
+
+ @Schema(description = "用户编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "23788")
+ @NotNull(message = "用户编号不能为空")
+ private Long userId;
+
+ @Schema(description = "变动余额,正数为增加,负数为减少", requiredMode = Schema.RequiredMode.REQUIRED, example = "100")
+ @NotNull(message = "变动余额不能为空")
+ private Integer balance;
+
+}
diff --git a/yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/service/wallet/PayWalletServiceImpl.java b/yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/service/wallet/PayWalletServiceImpl.java
index 513786143..b844e3769 100644
--- a/yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/service/wallet/PayWalletServiceImpl.java
+++ b/yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/service/wallet/PayWalletServiceImpl.java
@@ -12,12 +12,12 @@ import cn.iocoder.yudao.module.pay.enums.wallet.PayWalletBizTypeEnum;
import cn.iocoder.yudao.module.pay.service.order.PayOrderService;
import cn.iocoder.yudao.module.pay.service.refund.PayRefundService;
import cn.iocoder.yudao.module.pay.service.wallet.bo.WalletTransactionCreateReqBO;
+import jakarta.annotation.Resource;
import lombok.extern.slf4j.Slf4j;
import org.springframework.context.annotation.Lazy;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
-import jakarta.annotation.Resource;
import java.time.LocalDateTime;
import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception;
@@ -176,6 +176,9 @@ public class PayWalletServiceImpl implements PayWalletService {
walletMapper.updateWhenRecharge(payWallet.getId(), price);
break;
}
+ case UPDATE_BALANCE: // 更新余额
+ walletMapper.updateWhenRecharge(payWallet.getId(), price);
+ break;
default: {
// TODO 其它类型待实现
throw new UnsupportedOperationException("待实现");
diff --git a/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/sms/SmsCallbackController.java b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/sms/SmsCallbackController.java
index 4b68e7b6b..86a82537a 100644
--- a/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/sms/SmsCallbackController.java
+++ b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/sms/SmsCallbackController.java
@@ -6,9 +6,7 @@ import cn.iocoder.yudao.module.system.framework.sms.core.enums.SmsChannelEnum;
import cn.iocoder.yudao.module.system.service.sms.SmsSendService;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
-import org.springframework.web.bind.annotation.PostMapping;
-import org.springframework.web.bind.annotation.RequestMapping;
-import org.springframework.web.bind.annotation.RestController;
+import org.springframework.web.bind.annotation.*;
import jakarta.annotation.Resource;
import jakarta.annotation.security.PermitAll;
@@ -26,7 +24,7 @@ public class SmsCallbackController {
@PostMapping("/aliyun")
@PermitAll
- @Operation(summary = "阿里云短信的回调", description = "参见 https://help.aliyun.com/zh/sms/developer-reference/configure-delivery-receipts-1 文档")
+ @Operation(summary = "阿里云短信的回调", description = "参见 https://help.aliyun.com/document_detail/120998.html 文档")
public CommonResult receiveAliyunSmsStatus(HttpServletRequest request) throws Throwable {
String text = ServletUtils.getBody(request);
smsSendService.receiveSmsStatus(SmsChannelEnum.ALIYUN.getCode(), text);
@@ -35,7 +33,7 @@ public class SmsCallbackController {
@PostMapping("/tencent")
@PermitAll
- @Operation(summary = "腾讯云短信的回调", description = "参见 https://cloud.tencent.com/document/product/382/59178 文档")
+ @Operation(summary = "腾讯云短信的回调", description = "参见 https://cloud.tencent.com/document/product/382/52077 文档")
public CommonResult receiveTencentSmsStatus(HttpServletRequest request) throws Throwable {
String text = ServletUtils.getBody(request);
smsSendService.receiveSmsStatus(SmsChannelEnum.TENCENT.getCode(), text);
@@ -46,10 +44,17 @@ public class SmsCallbackController {
@PostMapping("/huawei")
@PermitAll
@Operation(summary = "华为云短信的回调", description = "参见 https://support.huaweicloud.com/api-msgsms/sms_05_0003.html 文档")
- public CommonResult receiveHuaweiSmsStatus(HttpServletRequest request) throws Throwable {
- String text = ServletUtils.getBody(request);
- smsSendService.receiveSmsStatus(SmsChannelEnum.HUAWEI.getCode(), text);
+ public CommonResult receiveHuaweiSmsStatus(@RequestBody String requestBody) throws Throwable {
+ smsSendService.receiveSmsStatus(SmsChannelEnum.HUAWEI.getCode(), requestBody);
return success(true);
}
-}
+ @PostMapping("/qiniu")
+ @PermitAll
+ @Operation(summary = "七牛云短信的回调", description = "参见 https://developer.qiniu.com/sms/5910/message-push 文档")
+ public CommonResult receiveQiniuSmsStatus(@RequestBody String requestBody) throws Throwable {
+ smsSendService.receiveSmsStatus(SmsChannelEnum.QINIU.getCode(), requestBody);
+ return success(true);
+ }
+
+}
\ No newline at end of file
diff --git a/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/framework/sms/core/client/SmsClientFactory.java b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/framework/sms/core/client/SmsClientFactory.java
index a1133177f..ad878b78e 100644
--- a/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/framework/sms/core/client/SmsClientFactory.java
+++ b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/framework/sms/core/client/SmsClientFactory.java
@@ -30,7 +30,8 @@ public interface SmsClientFactory {
* 创建短信 Client
*
* @param properties 配置对象
+ * @return 短信 Client
*/
- void createOrUpdateSmsClient(SmsChannelProperties properties);
+ SmsClient createOrUpdateSmsClient(SmsChannelProperties properties);
}
diff --git a/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/framework/sms/core/client/impl/AbstractSmsClient.java b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/framework/sms/core/client/impl/AbstractSmsClient.java
index 3b6e0eb0d..a1883bfdf 100644
--- a/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/framework/sms/core/client/impl/AbstractSmsClient.java
+++ b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/framework/sms/core/client/impl/AbstractSmsClient.java
@@ -26,15 +26,9 @@ public abstract class AbstractSmsClient implements SmsClient {
* 初始化
*/
public final void init() {
- doInit();
log.debug("[init][配置({}) 初始化完成]", properties);
}
- /**
- * 自定义初始化
- */
- protected abstract void doInit();
-
public final void refresh(SmsChannelProperties properties) {
// 判断是否更新
if (properties.equals(this.properties)) {
diff --git a/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/framework/sms/core/client/impl/AliyunSmsClient.java b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/framework/sms/core/client/impl/AliyunSmsClient.java
index f8158cdf2..558dbdef2 100644
--- a/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/framework/sms/core/client/impl/AliyunSmsClient.java
+++ b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/framework/sms/core/client/impl/AliyunSmsClient.java
@@ -50,10 +50,6 @@ public class AliyunSmsClient extends AbstractSmsClient {
Assert.notEmpty(properties.getApiSecret(), "apiSecret 不能为空");
}
- @Override
- protected void doInit() {
- }
-
@Override
public SmsSendRespDTO sendSms(Long sendLogId, String mobile, String apiTemplateId,
List> templateParams) throws Throwable {
@@ -80,7 +76,7 @@ public class AliyunSmsClient extends AbstractSmsClient {
@Override
public List parseSmsReceiveStatus(String text) {
JSONArray statuses = JSONUtil.parseArray(text);
- // 字段参考
+ // 字段参考 https://help.aliyun.com/zh/sms/developer-reference/smsreport-2
return convertList(statuses, status -> {
JSONObject statusObj = (JSONObject) status;
return new SmsReceiveRespDTO()
@@ -166,7 +162,8 @@ public class AliyunSmsClient extends AbstractSmsClient {
String hashedRequestBody = DigestUtil.sha256Hex(requestBody);
// 4. 构建 Authorization 签名
- String canonicalRequest = "POST" + "\n" + "/" + "\n" + queryString + "\n" + canonicalHeaders + "\n" + signedHeaders + "\n" + hashedRequestBody;
+ String canonicalRequest = "POST" + "\n" + "/" + "\n" + queryString + "\n"
+ + canonicalHeaders + "\n" + signedHeaders + "\n" + hashedRequestBody;
String hashedCanonicalRequest = DigestUtil.sha256Hex(canonicalRequest);
String stringToSign = "ACS3-HMAC-SHA256" + "\n" + hashedCanonicalRequest;
String signature = SecureUtil.hmacSha256(properties.getApiSecret()).digestHex(stringToSign); // 计算签名
diff --git a/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/framework/sms/core/client/impl/DebugDingTalkSmsClient.java b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/framework/sms/core/client/impl/DebugDingTalkSmsClient.java
index e9fcc6c41..6d2f2d017 100644
--- a/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/framework/sms/core/client/impl/DebugDingTalkSmsClient.java
+++ b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/framework/sms/core/client/impl/DebugDingTalkSmsClient.java
@@ -36,10 +36,6 @@ public class DebugDingTalkSmsClient extends AbstractSmsClient {
Assert.notEmpty(properties.getApiSecret(), "apiSecret 不能为空");
}
- @Override
- protected void doInit() {
- }
-
@Override
public SmsSendRespDTO sendSms(Long sendLogId, String mobile,
String apiTemplateId, List> templateParams) throws Throwable {
diff --git a/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/framework/sms/core/client/impl/HuaweiSmsClient.java b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/framework/sms/core/client/impl/HuaweiSmsClient.java
index fdf2faa1a..82f55395e 100644
--- a/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/framework/sms/core/client/impl/HuaweiSmsClient.java
+++ b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/framework/sms/core/client/impl/HuaweiSmsClient.java
@@ -1,41 +1,35 @@
package cn.iocoder.yudao.module.system.framework.sms.core.client.impl;
+import cn.hutool.core.collection.ListUtil;
+import cn.hutool.core.date.format.FastDateFormat;
import cn.hutool.core.lang.Assert;
+import cn.hutool.core.util.CharsetUtil;
import cn.hutool.core.util.StrUtil;
-
import cn.hutool.crypto.SecureUtil;
-import cn.hutool.http.HttpRequest;
-import cn.hutool.http.HttpResponse;
+import cn.hutool.http.HttpUtil;
import cn.hutool.json.JSONObject;
import cn.hutool.json.JSONUtil;
import cn.iocoder.yudao.framework.common.core.KeyValue;
-import cn.iocoder.yudao.framework.common.util.collection.CollectionUtils;
+import cn.iocoder.yudao.framework.common.util.http.HttpUtils;
import cn.iocoder.yudao.framework.common.util.json.JsonUtils;
import cn.iocoder.yudao.module.system.framework.sms.core.client.dto.SmsReceiveRespDTO;
import cn.iocoder.yudao.module.system.framework.sms.core.client.dto.SmsSendRespDTO;
import cn.iocoder.yudao.module.system.framework.sms.core.client.dto.SmsTemplateRespDTO;
import cn.iocoder.yudao.module.system.framework.sms.core.enums.SmsTemplateAuditStatusEnum;
import cn.iocoder.yudao.module.system.framework.sms.core.property.SmsChannelProperties;
-
-import com.fasterxml.jackson.annotation.JsonFormat;
-import com.fasterxml.jackson.annotation.JsonProperty;
-import lombok.Data;
import lombok.extern.slf4j.Slf4j;
import java.io.UnsupportedEncodingException;
import java.net.URLEncoder;
-import java.text.SimpleDateFormat;
-import java.util.*;
-
-
+import java.nio.charset.StandardCharsets;
+import java.time.Instant;
import java.time.LocalDateTime;
+import java.time.ZoneId;
+import java.util.*;
import static cn.hutool.crypto.digest.DigestUtil.sha256Hex;
import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertList;
-import static cn.iocoder.yudao.framework.common.util.date.DateUtils.FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND;
-import static cn.iocoder.yudao.framework.common.util.date.DateUtils.TIME_ZONE_DEFAULT;
-// todo @scholar:参考阿里云在优化下
/**
* 华为短信客户端的实现类
*
@@ -45,182 +39,128 @@ import static cn.iocoder.yudao.framework.common.util.date.DateUtils.TIME_ZONE_DE
@Slf4j
public class HuaweiSmsClient extends AbstractSmsClient {
- /**
- * 调用成功 code
- */
- public static final String URL = "https://smsapi.cn-north-4.myhuaweicloud.com:443/sms/batchSendSms/v1";//APP接入地址+接口访问URI
- public static final String HOST = "smsapi.cn-north-4.myhuaweicloud.com:443";
- public static final String SIGNEDHEADERS = "content-type;host;x-sdk-date";
+ private static final String URL = "https://smsapi.cn-north-4.myhuaweicloud.com:443/sms/batchSendSms/v1";//APP接入地址+接口访问URI
+ private static final String HOST = "smsapi.cn-north-4.myhuaweicloud.com:443";
+ private static final String SIGNEDHEADERS = "content-type;host;x-sdk-date";
- @Override
- protected void doInit() {
- }
+ private static final String RESPONSE_CODE_SUCCESS = "000000";
public HuaweiSmsClient(SmsChannelProperties properties) {
super(properties);
Assert.notEmpty(properties.getApiKey(), "apiKey 不能为空");
Assert.notEmpty(properties.getApiSecret(), "apiSecret 不能为空");
+ validateSender(properties);
+ }
+
+ /**
+ * 参数校验华为云的 sender 通道号
+ *
+ * 原因是:验华为云发放短信的时候,需要额外的参数 sender
+ *
+ * 解决方案:考虑到不破坏原有的 apiKey + apiSecret 的结构,所以将 secretId 拼接到 apiKey 字段中,格式为 "secretId sdkAppId"。
+ *
+ * @param properties 配置
+ */
+ private static void validateSender(SmsChannelProperties properties) {
+ String combineKey = properties.getApiKey();
+ Assert.notEmpty(combineKey, "apiKey 不能为空");
+ String[] keys = combineKey.trim().split(" ");
+ Assert.isTrue(keys.length == 2, "华为云短信 apiKey 配置格式错误,请配置 为[accessKeyId sender]");
+ }
+
+ private String getAccessKey() {
+ return StrUtil.subBefore(properties.getApiKey(), " ", true);
+ }
+
+ private String getSender() {
+ return StrUtil.subAfter(properties.getApiKey(), " ", true);
}
@Override
public SmsSendRespDTO sendSms(Long sendLogId, String mobile, String apiTemplateId,
List> templateParams) throws Throwable {
- // 参考链接 https://support.huaweicloud.com/api-msgsms/sms_05_0001.html
- // 相比较阿里短信,华为短信发送的时候需要额外的参数“通道号”,考虑到不破坏原有的的结构
- // 所以将 通道号 拼接到 apiTemplateId 字段中,格式为 "apiTemplateId 通道号"。空格为分隔符。
- String sender = StrUtil.subAfter(apiTemplateId, " ", true); //中国大陆短信签名通道号或全球短信通道号
- String templateId = StrUtil.subBefore(apiTemplateId, " ", true); //模板ID
+ StringBuilder requestBody = new StringBuilder();
+ appendToBody(requestBody, "from=", getSender());
+ appendToBody(requestBody, "&to=", mobile);
+ appendToBody(requestBody, "&templateId=", apiTemplateId);
+ appendToBody(requestBody, "&templateParas=", JsonUtils.toJsonString(
+ convertList(templateParams, kv -> String.valueOf(kv.getValue()))));
+ appendToBody(requestBody, "&statusCallback=", properties.getCallbackUrl());
+ appendToBody(requestBody, "&extend=", String.valueOf(sendLogId));
+ JSONObject response = request("/sms/batchSendSms/v1/", "POST", requestBody.toString());
- //选填,短信状态报告接收地址,推荐使用域名,为空或者不填表示不接收状态报告
- String statusCallBack = properties.getCallbackUrl();
-
- List templateParas = CollectionUtils.convertList(templateParams, kv -> String.valueOf(kv.getValue()));
-
- JSONObject JsonResponse = sendSmsRequest(sender,mobile,templateId,templateParas,statusCallBack);
- SmsResponse smsResponse = getSmsSendResponse(JsonResponse);
-
- return new SmsSendRespDTO().setSuccess(smsResponse.success).setApiMsg(smsResponse.data.toString());
+ // 2. 解析请求
+ if (!response.containsKey("result")) { // 例如说:密钥不正确
+ return new SmsSendRespDTO().setSuccess(false)
+ .setApiCode(response.getStr("code"))
+ .setApiMsg(response.getStr("description"));
+ }
+ JSONObject sendResult = response.getJSONArray("result").getJSONObject(0);
+ return new SmsSendRespDTO().setSuccess(RESPONSE_CODE_SUCCESS.equals(response.getStr("code")))
+ .setSerialNo(sendResult.getStr("smsMsgId")).setApiCode(sendResult.getStr("status"));
}
- JSONObject sendSmsRequest(String sender,String mobile,String templateId,List templateParas,String statusCallBack) throws UnsupportedEncodingException {
+ /**
+ * 请求华为云短信
+ *
+ * @see https://support.huaweicloud.com/api-msgsms/sms_05_0046.html
+ * @param uri 请求 URI
+ * @param method 请求 Method
+ * @param requestBody 请求 Body
+ * @return 请求结果
+ */
+ private JSONObject request(String uri, String method, String requestBody) {
+ // 1.1 请求 Header
+ TreeMap headers = new TreeMap<>();
+ headers.put("Content-Type", "application/x-www-form-urlencoded");
+ String sdkDate = FastDateFormat.getInstance("yyyyMMdd'T'HHmmss'Z'", TimeZone.getTimeZone("UTC")).format(new Date());
+ headers.put("X-Sdk-Date", sdkDate);
+ headers.put("host", HOST);
- SimpleDateFormat sdf = new SimpleDateFormat("yyyyMMdd'T'HHmmss'Z'", Locale.ENGLISH);
- sdf.setTimeZone(TimeZone.getTimeZone("UTC"));
- String sdkDate = sdf.format(new Date());
-
- // ************* 步骤 1:拼接规范请求串 *************
- String httpRequestMethod = "POST";
- String canonicalUri = "/sms/batchSendSms/v1/";
- String canonicalQueryString = "";//查询参数为空
+ // 1.2 构建签名 Header
+ String canonicalQueryString = ""; // 查询参数为空
String canonicalHeaders = "content-type:application/x-www-form-urlencoded\n"
- + "host:"+ HOST +"\n"
- + "x-sdk-date:" + sdkDate + "\n";
- //请求Body,不携带签名名称时,signature请填null
- String body = buildRequestBody(sender, mobile, templateId, templateParas, statusCallBack, null);
- if (null == body || body.isEmpty()) {
- return null;
- }
- String hashedRequestBody = sha256Hex(body);
- String canonicalRequest = httpRequestMethod + "\n" + canonicalUri + "\n" + canonicalQueryString + "\n"
- + canonicalHeaders + "\n" + SIGNEDHEADERS + "\n" + hashedRequestBody;
+ + "host:"+ HOST +"\n" + "x-sdk-date:" + sdkDate + "\n";
+ String canonicalRequest = method + "\n" + uri + "\n" + canonicalQueryString + "\n"
+ + canonicalHeaders + "\n" + SIGNEDHEADERS + "\n" + sha256Hex(requestBody);
+ String stringToSign = "SDK-HMAC-SHA256" + "\n" + sdkDate + "\n" + sha256Hex(canonicalRequest);
+ String signature = SecureUtil.hmacSha256(properties.getApiSecret()).digestHex(stringToSign); // 计算签名
+ headers.put("Authorization", "SDK-HMAC-SHA256" + " " + "Access=" + getAccessKey()
+ + ", " + "SignedHeaders=" + SIGNEDHEADERS + ", " + "Signature=" + signature);
- // ************* 步骤 2:拼接待签名字符串 *************
- String hashedCanonicalRequest = sha256Hex(canonicalRequest);
- String stringToSign = "SDK-HMAC-SHA256" + "\n" + sdkDate + "\n" + hashedCanonicalRequest;
-
- // ************* 步骤 3:计算签名 *************
- String signature = SecureUtil.hmacSha256(properties.getApiSecret()).digestHex(stringToSign);
-
- // ************* 步骤 4:拼接 Authorization *************
- String authorization = "SDK-HMAC-SHA256" + " " + "Access=" + properties.getApiKey() + ", "
- + "SignedHeaders=" + SIGNEDHEADERS + ", " + "Signature=" + signature;
-
- // ************* 步骤 5:构造HttpRequest 并执行request请求,获得response *************
- HttpResponse response = HttpRequest.post(URL)
- .header("Content-Type", "application/x-www-form-urlencoded")
- .header("X-Sdk-Date", sdkDate)
- .header("host",HOST)
- .header("Authorization", authorization)
- .body(body)
- .execute();
-
- return JSONUtil.parseObj(response.body());
+ // 2. 发起请求
+ String responseBody = HttpUtils.post(URL, headers, requestBody);
+ return JSONUtil.parseObj(responseBody);
}
- private SmsResponse getSmsSendResponse(JSONObject resJson) {
- SmsResponse smsResponse = new SmsResponse();
- smsResponse.setSuccess("000000".equals(resJson.getStr("code")));
- smsResponse.setData(resJson);
- return smsResponse;
- }
-
- static String buildRequestBody(String sender, String receiver, String templateId, List templateParas,
- String statusCallBack, String signature) throws UnsupportedEncodingException {
- if (null == sender || null == receiver || null == templateId || sender.isEmpty() || receiver.isEmpty()
- || templateId.isEmpty()) {
- System.out.println("buildRequestBody(): sender, receiver or templateId is null.");
- return null;
- }
-
- StringBuilder body = new StringBuilder();
- appendToBody(body, "from=", sender);
- appendToBody(body, "&to=", receiver);
- appendToBody(body, "&templateId=", templateId);
- appendToBody(body, "&templateParas=", JsonUtils.toJsonString(templateParas));
- appendToBody(body, "&statusCallback=", statusCallBack);
- appendToBody(body, "&signature=", signature);
- return body.toString();
- }
-
- private static void appendToBody(StringBuilder body, String key, String val) throws UnsupportedEncodingException {
- if (null != val && !val.isEmpty()) {
- body.append(key).append(URLEncoder.encode(val, "UTF-8"));
- }
- }
@Override
- public List parseSmsReceiveStatus(String text) {
- List statuses = JsonUtils.parseArray(text, SmsReceiveStatus.class);
- return convertList(statuses, status -> new SmsReceiveRespDTO().setSuccess(Objects.equals(status.getStatus(),"DELIVRD"))
- .setErrorCode(status.getStatus()).setErrorMsg(status.getStatus())
- .setMobile(status.getPhoneNumber()).setReceiveTime(status.getUpdateTime())
- .setSerialNo(status.getSmsMsgId()));
+ public List parseSmsReceiveStatus(String requestBody) {
+ Map params = HttpUtil.decodeParamMap(requestBody, StandardCharsets.UTF_8);
+ // 字段参考 https://support.huaweicloud.com/api-msgsms/sms_05_0003.html
+ return ListUtil.of(new SmsReceiveRespDTO()
+ .setSuccess("DELIVRD".equals(params.get("status"))) // 是否接收成功
+ .setErrorCode(params.get("status")) // 状态报告编码
+ .setErrorMsg(params.get("statusDesc"))
+ .setMobile(params.get("to")) // 手机号
+ .setReceiveTime(LocalDateTime.ofInstant(Instant.parse(params.get("updateTime")), ZoneId.of("UTC"))) // 状态报告时间
+ .setSerialNo(params.get("smsMsgId")) // 发送序列号
+ .setLogId(Long.valueOf(params.get("extend")))); // 用户序列号
}
@Override
public SmsTemplateRespDTO getSmsTemplate(String apiTemplateId) throws Throwable {
- //华为短信模板查询和发送短信,是不同的两套key和secret,与阿里、腾讯的区别较大,这里模板查询校验暂不实现。
- return new SmsTemplateRespDTO().setId(null).setContent(null)
+ // 华为短信模板查询和发送短信,是不同的两套 key 和 secret,与阿里、腾讯的区别较大,这里模板查询校验暂不实现
+ String[] strs = apiTemplateId.split(" ");
+ Assert.isTrue(strs.length == 2, "格式不正确,需要满足:apiTemplateId sender");
+ return new SmsTemplateRespDTO().setId(apiTemplateId).setContent(null)
.setAuditStatus(SmsTemplateAuditStatusEnum.SUCCESS.getStatus()).setAuditReason(null);
-
}
- @Data
- public static class SmsResponse {
-
- /**
- * 是否成功
- */
- private boolean success;
-
- /**
- * 厂商原返回体
- */
- private Object data;
-
+ @SuppressWarnings("CharsetObjectCanBeUsed")
+ private static void appendToBody(StringBuilder body, String key, String value) throws UnsupportedEncodingException {
+ if (StrUtil.isNotEmpty(value)) {
+ body.append(key).append(URLEncoder.encode(value, CharsetUtil.CHARSET_UTF_8.name()));
+ }
}
-
- /**
- * 短信接收状态
- *
- * 参见 文档
- *
- * @author scholar
- */
- @Data
- public static class SmsReceiveStatus {
-
- /**
- * 本条状态报告对应的短信的接收方号码,仅当状态报告中携带了extend参数时才会同时携带该参数
- */
- @JsonProperty("to")
- private String phoneNumber;
-
- /**
- * 短信资源的更新时间,通常为短信平台接收短信状态报告的时间
- */
- @JsonFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND, timezone = TIME_ZONE_DEFAULT)
- private LocalDateTime updateTime;
-
- /**
- * 短信状态报告枚举值
- */
- private String status;
-
- /**
- * 发送短信成功时返回的短信唯一标识。
- */
- private String smsMsgId;
- }
-
-}
+}
\ No newline at end of file
diff --git a/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/framework/sms/core/client/impl/QiniuSmsClient.java b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/framework/sms/core/client/impl/QiniuSmsClient.java
new file mode 100644
index 000000000..a041970be
--- /dev/null
+++ b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/framework/sms/core/client/impl/QiniuSmsClient.java
@@ -0,0 +1,155 @@
+package cn.iocoder.yudao.module.system.framework.sms.core.client.impl;
+
+import cn.hutool.core.collection.CollStreamUtil;
+import cn.hutool.core.date.DateUtil;
+import cn.hutool.core.date.LocalDateTimeUtil;
+import cn.hutool.core.lang.Assert;
+import cn.hutool.core.util.ObjectUtil;
+import cn.hutool.crypto.SecureUtil;
+import cn.hutool.crypto.digest.HmacAlgorithm;
+import cn.hutool.json.JSONObject;
+import cn.hutool.json.JSONUtil;
+import cn.iocoder.yudao.framework.common.core.KeyValue;
+import cn.iocoder.yudao.framework.common.util.http.HttpUtils;
+import cn.iocoder.yudao.module.system.framework.sms.core.client.dto.SmsReceiveRespDTO;
+import cn.iocoder.yudao.module.system.framework.sms.core.client.dto.SmsSendRespDTO;
+import cn.iocoder.yudao.module.system.framework.sms.core.client.dto.SmsTemplateRespDTO;
+import cn.iocoder.yudao.module.system.framework.sms.core.enums.SmsTemplateAuditStatusEnum;
+import cn.iocoder.yudao.module.system.framework.sms.core.property.SmsChannelProperties;
+import com.google.common.annotations.VisibleForTesting;
+import lombok.extern.slf4j.Slf4j;
+
+import java.util.*;
+import java.util.function.Function;
+
+import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertList;
+
+/**
+ * 七牛云短信客户端的实现类
+ *
+ * @author scholar
+ * @since 2024/08/26 15:35
+ */
+@Slf4j
+public class QiniuSmsClient extends AbstractSmsClient {
+
+ private static final String HOST = "sms.qiniuapi.com";
+
+ public QiniuSmsClient(SmsChannelProperties properties) {
+ super(properties);
+ Assert.notEmpty(properties.getApiKey(), "apiKey 不能为空");
+ Assert.notEmpty(properties.getApiSecret(), "apiSecret 不能为空");
+ }
+
+ public SmsSendRespDTO sendSms(Long sendLogId, String mobile, String apiTemplateId,
+ List> templateParams) throws Throwable {
+ // 1. 执行请求
+ // 参考链接 https://developer.qiniu.com/sms/5824/through-the-api-send-text-messages
+ LinkedHashMap body = new LinkedHashMap<>();
+ body.put("template_id", apiTemplateId);
+ body.put("mobile", mobile);
+ body.put("parameters", CollStreamUtil.toMap(templateParams, KeyValue::getKey, KeyValue::getValue));
+ body.put("seq", Long.toString(sendLogId));
+ JSONObject response = request("POST", body, "/v1/message/single");
+
+ // 2. 解析请求
+ if (ObjectUtil.isNotEmpty(response.getStr("error"))) {
+ // 短信请求失败
+ return new SmsSendRespDTO().setSuccess(false)
+ .setApiCode(response.getStr("error"))
+ .setApiRequestId(response.getStr("request_id"))
+ .setApiMsg(response.getStr("message"));
+ }
+ return new SmsSendRespDTO().setSuccess(response.containsKey("message_id"))
+ .setSerialNo(response.getStr("message_id"));
+ }
+
+ /**
+ * 请求七牛云短信
+ *
+ * @see
+ * @param httpMethod http请求方法
+ * @param body http请求消息体
+ * @param path URL path
+ * @return 请求结果
+ */
+ private JSONObject request(String httpMethod, LinkedHashMap body, String path) {
+ String signDate = DateUtil.date().setTimeZone(TimeZone.getTimeZone("UTC")).toString("yyyyMMdd'T'HHmmss'Z'");
+ // 1. 请求头
+ Map header = new HashMap<>(4);
+ header.put("HOST", HOST);
+ header.put("Authorization", getSignature(httpMethod, path, body != null ? JSONUtil.toJsonStr(body) : "", signDate));
+ header.put("Content-Type", "application/json");
+ header.put("X-Qiniu-Date", signDate);
+
+ // 2. 发起请求
+ String responseBody;
+ if (Objects.equals(httpMethod, "POST")){
+ responseBody = HttpUtils.post("https://" + HOST + path, header, JSONUtil.toJsonStr(body));
+ } else {
+ responseBody = HttpUtils.get("https://" + HOST + path, header);
+ }
+ return JSONUtil.parseObj(responseBody);
+ }
+
+ private String getSignature(String method, String path, String body, String signDate) {
+ StringBuilder dataToSign = new StringBuilder();
+ dataToSign.append(method.toUpperCase()).append(" ").append(path)
+ .append("\nHost: ").append(HOST)
+ .append("\n").append("Content-Type").append(": ").append("application/json")
+ .append("\n").append("X-Qiniu-Date").append(": ").append(signDate)
+ .append("\n\n");
+ if (ObjectUtil.isNotEmpty(body)) {
+ dataToSign.append(body);
+ }
+ String signature = SecureUtil.hmac(HmacAlgorithm.HmacSHA1, properties.getApiSecret())
+ .digestBase64(dataToSign.toString(), true);
+ return "Qiniu " + properties.getApiKey() + ":" + signature;
+ }
+
+ @Override
+ public List parseSmsReceiveStatus(String text) {
+ JSONObject status = JSONUtil.parseObj(text);
+ // 字段参考 https://developer.qiniu.com/sms/5910/message-push
+ return convertList(status.getJSONArray("items"), new Function