Commit 30fd7996f6f2d1ed630a9c72460556ab34cb7bfc

Authored by YunaiV
2 parents 2759a323 15f46db7

Merge branch 'develop' of https://gitee.com/zhijiantianya/ruoyi-vue-pro into master-jdk17

Showing 71 changed files with 1622 additions and 916 deletions
@@ -15,12 +15,12 @@ @@ -15,12 +15,12 @@
15 <!-- 各种 module 拓展 --> 15 <!-- 各种 module 拓展 -->
16 <module>yudao-module-system</module> 16 <module>yudao-module-system</module>
17 <module>yudao-module-infra</module> 17 <module>yudao-module-infra</module>
18 -<!-- <module>yudao-module-member</module>--> 18 + <module>yudao-module-member</module>
19 <!-- <module>yudao-module-bpm</module>--> 19 <!-- <module>yudao-module-bpm</module>-->
20 <!-- <module>yudao-module-report</module>--> 20 <!-- <module>yudao-module-report</module>-->
21 <!-- <module>yudao-module-mp</module>--> 21 <!-- <module>yudao-module-mp</module>-->
22 -<!-- <module>yudao-module-pay</module>-->  
23 -<!-- <module>yudao-module-mall</module>--> 22 + <module>yudao-module-pay</module>
  23 + <module>yudao-module-mall</module>
24 <!-- <module>yudao-module-crm</module>--> 24 <!-- <module>yudao-module-crm</module>-->
25 <!-- <module>yudao-module-erp</module>--> 25 <!-- <module>yudao-module-erp</module>-->
26 <!-- <module>yudao-module-ai</module>--> 26 <!-- <module>yudao-module-ai</module>-->
yudao-framework/yudao-common/src/main/java/cn/iocoder/yudao/framework/common/util/http/HttpUtils.java
@@ -111,7 +111,7 @@ public class HttpUtils { @@ -111,7 +111,7 @@ public class HttpUtils {
111 authorization = Base64.decodeStr(authorization); 111 authorization = Base64.decodeStr(authorization);
112 clientId = StrUtil.subBefore(authorization, ":", false); 112 clientId = StrUtil.subBefore(authorization, ":", false);
113 clientSecret = StrUtil.subAfter(authorization, ":", false); 113 clientSecret = StrUtil.subAfter(authorization, ":", false);
114 - // 再从 Param 中获取 114 + // 再从 Param 中获取
115 } else { 115 } else {
116 clientId = request.getParameter("client_id"); 116 clientId = request.getParameter("client_id");
117 clientSecret = request.getParameter("client_secret"); 117 clientSecret = request.getParameter("client_secret");
@@ -143,4 +143,21 @@ public class HttpUtils { @@ -143,4 +143,21 @@ public class HttpUtils {
143 } 143 }
144 } 144 }
145 145
  146 + /**
  147 + * HTTP get 请求,基于 {@link cn.hutool.http.HttpUtil} 实现
  148 + *
  149 + * 为什么要封装该方法,因为 HttpUtil 默认封装的方法,没有允许传递 headers 参数
  150 + *
  151 + * @param url URL
  152 + * @param headers 请求头
  153 + * @return 请求结果
  154 + */
  155 + public static String get(String url, Map<String, String> headers) {
  156 + try (HttpResponse response = HttpRequest.get(url)
  157 + .addHeaders(headers)
  158 + .execute()) {
  159 + return response.body();
  160 + }
  161 + }
  162 +
146 } 163 }
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&lt;S3FileClientConfig&gt; { @@ -91,7 +91,7 @@ public class S3FileClient extends AbstractFileClient&lt;S3FileClientConfig&gt; {
91 * 开启 VirtualStyle 模式 91 * 开启 VirtualStyle 模式
92 */ 92 */
93 private void enableVirtualStyleEndpoint() { 93 private void enableVirtualStyleEndpoint() {
94 - if (StrUtil.containsAll(config.getEndpoint(), 94 + if (StrUtil.containsAny(config.getEndpoint(),
95 S3FileClientConfig.ENDPOINT_TENCENT, // 腾讯云 https://cloud.tencent.com/document/product/436/41284 95 S3FileClientConfig.ENDPOINT_TENCENT, // 腾讯云 https://cloud.tencent.com/document/product/436/41284
96 S3FileClientConfig.ENDPOINT_VOLCES)) { // 火山云 https://www.volcengine.com/docs/6349/1288493 96 S3FileClientConfig.ENDPOINT_VOLCES)) { // 火山云 https://www.volcengine.com/docs/6349/1288493
97 client.enableVirtualStyleEndpoint(); 97 client.enableVirtualStyleEndpoint();
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 @@ -286,6 +286,7 @@ public class ${table.className}ServiceImpl implements ${table.className}Service
286 // 校验存在 286 // 校验存在
287 validate${subSimpleClassName}Exists(${subClassNameVar}.getId()); 287 validate${subSimpleClassName}Exists(${subClassNameVar}.getId());
288 // 更新 288 // 更新
  289 + ${subClassNameVar}.setUpdater(null).setUpdateTime(null); // 解决更新情况下:updateTime 不更新
289 ${subClassNameVars.get($index)}Mapper.updateById(${subClassNameVar}); 290 ${subClassNameVars.get($index)}Mapper.updateById(${subClassNameVar});
290 } 291 }
291 292
yudao-module-infra/yudao-module-infra-biz/src/main/resources/codegen/vue3/views/components/form_sub_erp.vue.vm
@@ -64,12 +64,11 @@ @@ -64,12 +64,11 @@
64 <el-checkbox 64 <el-checkbox
65 v-for="dict in $dictMethod(DICT_TYPE.$dictType.toUpperCase())" 65 v-for="dict in $dictMethod(DICT_TYPE.$dictType.toUpperCase())"
66 :key="dict.value" 66 :key="dict.value"
67 - :label="dict.value"  
68 - >  
69 - {{ dict.label }}  
70 - </el-checkbox> 67 + :label="dict.label"
  68 + :value="dict.value"
  69 + />
71 #else##没数据字典 70 #else##没数据字典
72 - <el-checkbox>请选择字典生成</el-checkbox> 71 + <el-checkbox label="请选择字典生成" />
73 #end 72 #end
74 </el-checkbox-group> 73 </el-checkbox-group>
75 </el-form-item> 74 </el-form-item>
@@ -85,7 +84,7 @@ @@ -85,7 +84,7 @@
85 {{ dict.label }} 84 {{ dict.label }}
86 </el-radio> 85 </el-radio>
87 #else##没数据字典 86 #else##没数据字典
88 - <el-radio label="1">请选择字典生成</el-radio> 87 + <el-radio value="1">请选择字典生成</el-radio>
89 #end 88 #end
90 </el-radio-group> 89 </el-radio-group>
91 </el-form-item> 90 </el-form-item>
yudao-module-infra/yudao-module-infra-biz/src/main/resources/codegen/vue3/views/components/form_sub_normal.vue.vm
@@ -92,12 +92,11 @@ @@ -92,12 +92,11 @@
92 <el-checkbox 92 <el-checkbox
93 v-for="dict in $dictMethod(DICT_TYPE.$dictType.toUpperCase())" 93 v-for="dict in $dictMethod(DICT_TYPE.$dictType.toUpperCase())"
94 :key="dict.value" 94 :key="dict.value"
95 - :label="dict.value"  
96 - >  
97 - {{ dict.label }}  
98 - </el-checkbox> 95 + :label="dict.label"
  96 + :value="dict.value"
  97 + />
99 #else##没数据字典 98 #else##没数据字典
100 - <el-checkbox>请选择字典生成</el-checkbox> 99 + <el-checkbox label="请选择字典生成" />
101 #end 100 #end
102 </el-checkbox-group> 101 </el-checkbox-group>
103 </el-form-item> 102 </el-form-item>
@@ -117,7 +116,7 @@ @@ -117,7 +116,7 @@
117 {{ dict.label }} 116 {{ dict.label }}
118 </el-radio> 117 </el-radio>
119 #else##没数据字典 118 #else##没数据字典
120 - <el-radio label="1">请选择字典生成</el-radio> 119 + <el-radio value="1">请选择字典生成</el-radio>
121 #end 120 #end
122 </el-radio-group> 121 </el-radio-group>
123 </el-form-item> 122 </el-form-item>
@@ -219,12 +218,11 @@ @@ -219,12 +218,11 @@
219 <el-checkbox 218 <el-checkbox
220 v-for="dict in $dictMethod(DICT_TYPE.$dictType.toUpperCase())" 219 v-for="dict in $dictMethod(DICT_TYPE.$dictType.toUpperCase())"
221 :key="dict.value" 220 :key="dict.value"
222 - :label="dict.value"  
223 - >  
224 - {{ dict.label }}  
225 - </el-checkbox> 221 + :label="dict.label"
  222 + :value="dict.value"
  223 + />
226 #else##没数据字典 224 #else##没数据字典
227 - <el-checkbox>请选择字典生成</el-checkbox> 225 + <el-checkbox label="请选择字典生成" />
228 #end 226 #end
229 </el-checkbox-group> 227 </el-checkbox-group>
230 </el-form-item> 228 </el-form-item>
@@ -240,7 +238,7 @@ @@ -240,7 +238,7 @@
240 {{ dict.label }} 238 {{ dict.label }}
241 </el-radio> 239 </el-radio>
242 #else##没数据字典 240 #else##没数据字典
243 - <el-radio label="1">请选择字典生成</el-radio> 241 + <el-radio value="1">请选择字典生成</el-radio>
244 #end 242 #end
245 </el-radio-group> 243 </el-radio-group>
246 </el-form-item> 244 </el-form-item>
yudao-module-infra/yudao-module-infra-biz/src/main/resources/codegen/vue3/views/form.vue.vm
@@ -75,12 +75,11 @@ @@ -75,12 +75,11 @@
75 <el-checkbox 75 <el-checkbox
76 v-for="dict in $dictMethod(DICT_TYPE.$dictType.toUpperCase())" 76 v-for="dict in $dictMethod(DICT_TYPE.$dictType.toUpperCase())"
77 :key="dict.value" 77 :key="dict.value"
78 - :label="dict.value"  
79 - >  
80 - {{ dict.label }}  
81 - </el-checkbox> 78 + :label="dict.label"
  79 + :value="dict.value"
  80 + />
82 #else##没数据字典 81 #else##没数据字典
83 - <el-checkbox>请选择字典生成</el-checkbox> 82 + <el-checkbox label="请选择字典生成" />
84 #end 83 #end
85 </el-checkbox-group> 84 </el-checkbox-group>
86 </el-form-item> 85 </el-form-item>
@@ -96,7 +95,7 @@ @@ -96,7 +95,7 @@
96 {{ dict.label }} 95 {{ dict.label }}
97 </el-radio> 96 </el-radio>
98 #else##没数据字典 97 #else##没数据字典
99 - <el-radio label="1">请选择字典生成</el-radio> 98 + <el-radio value="1">请选择字典生成</el-radio>
100 #end 99 #end
101 </el-radio-group> 100 </el-radio-group>
102 </el-form-item> 101 </el-form-item>
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; @@ -3,6 +3,8 @@ package cn.iocoder.yudao.module.product.api.spu.dto;
3 import cn.iocoder.yudao.module.product.enums.spu.ProductSpuStatusEnum; 3 import cn.iocoder.yudao.module.product.enums.spu.ProductSpuStatusEnum;
4 import lombok.Data; 4 import lombok.Data;
5 5
  6 +import java.util.List;
  7 +
6 /** 8 /**
7 * 商品 SPU 信息 Response DTO 9 * 商品 SPU 信息 Response DTO
8 * 10 *
@@ -69,6 +71,13 @@ public class ProductSpuRespDTO { @@ -69,6 +71,13 @@ public class ProductSpuRespDTO {
69 // ========== 物流相关字段 ========= 71 // ========== 物流相关字段 =========
70 72
71 /** 73 /**
  74 + * 配送方式数组
  75 + *
  76 + * 对应 DeliveryTypeEnum 枚举
  77 + */
  78 + private List<Integer> deliveryTypes;
  79 +
  80 + /**
72 * 物流配置模板编号 81 * 物流配置模板编号
73 * 82 *
74 * 对应 TradeDeliveryExpressTemplateDO 的 id 编号 83 * 对应 TradeDeliveryExpressTemplateDO 的 id 编号
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 { @@ -67,7 +67,7 @@ public class ProductCommentServiceImpl implements ProductCommentService {
67 // 校验 SPU 67 // 校验 SPU
68 ProductSpuDO spu = validateSpu(sku.getSpuId()); 68 ProductSpuDO spu = validateSpu(sku.getSpuId());
69 // 校验评论 69 // 校验评论
70 - validateCommentExists(createReqDTO.getUserId(), createReqDTO.getOrderId()); 70 + validateCommentExists(createReqDTO.getUserId(), createReqDTO.getOrderItemId());
71 // 获取用户详细信息 71 // 获取用户详细信息
72 MemberUserRespDTO user = memberUserApi.getUser(createReqDTO.getUserId()); 72 MemberUserRespDTO user = memberUserApi.getUser(createReqDTO.getUserId());
73 73
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; @@ -3,9 +3,11 @@ package cn.iocoder.yudao.module.promotion.api.coupon;
3 import cn.iocoder.yudao.module.promotion.api.coupon.dto.CouponRespDTO; 3 import cn.iocoder.yudao.module.promotion.api.coupon.dto.CouponRespDTO;
4 import cn.iocoder.yudao.module.promotion.api.coupon.dto.CouponUseReqDTO; 4 import cn.iocoder.yudao.module.promotion.api.coupon.dto.CouponUseReqDTO;
5 import cn.iocoder.yudao.module.promotion.api.coupon.dto.CouponValidReqDTO; 5 import cn.iocoder.yudao.module.promotion.api.coupon.dto.CouponValidReqDTO;
6 -  
7 import jakarta.validation.Valid; 6 import jakarta.validation.Valid;
8 7
  8 +import java.util.List;
  9 +import java.util.Map;
  10 +
9 /** 11 /**
10 * 优惠劵 API 接口 12 * 优惠劵 API 接口
11 * 13 *
@@ -35,4 +37,21 @@ public interface CouponApi { @@ -35,4 +37,21 @@ public interface CouponApi {
35 */ 37 */
36 CouponRespDTO validateCoupon(@Valid CouponValidReqDTO validReqDTO); 38 CouponRespDTO validateCoupon(@Valid CouponValidReqDTO validReqDTO);
37 39
  40 + /**
  41 + * 【管理员】给指定用户批量发送优惠券
  42 + *
  43 + * @param giveCoupons key: 优惠劵模版编号,value:对应的数量
  44 + * @param userId 用户编号
  45 + * @return 优惠券编号列表
  46 + */
  47 + List<Long> takeCouponsByAdmin(Map<Long, Integer> giveCoupons, Long userId);
  48 +
  49 + /**
  50 + * 【管理员】作废指定用户的指定优惠劵
  51 + *
  52 + * @param giveCouponIds 赠送的优惠券编号
  53 + * @param userId 用户编号
  54 + */
  55 + void invalidateCouponsByAdmin(List<Long> giveCouponIds, Long userId);
  56 +
38 } 57 }
yudao-module-mall/yudao-module-promotion-api/src/main/java/cn/iocoder/yudao/module/promotion/api/reward/dto/RewardActivityMatchRespDTO.java
1 package cn.iocoder.yudao.module.promotion.api.reward.dto; 1 package cn.iocoder.yudao.module.promotion.api.reward.dto;
2 2
  3 +import cn.iocoder.yudao.framework.common.enums.CommonStatusEnum;
3 import cn.iocoder.yudao.module.promotion.enums.common.PromotionConditionTypeEnum; 4 import cn.iocoder.yudao.module.promotion.enums.common.PromotionConditionTypeEnum;
  5 +import cn.iocoder.yudao.module.promotion.enums.common.PromotionProductScopeEnum;
4 import lombok.Data; 6 import lombok.Data;
5 7
  8 +import java.io.Serializable;
  9 +import java.time.LocalDateTime;
6 import java.util.List; 10 import java.util.List;
  11 +import java.util.Map;
7 12
8 /** 13 /**
9 * 满减送活动的匹配 Response DTO 14 * 满减送活动的匹配 Response DTO
@@ -22,27 +27,49 @@ public class RewardActivityMatchRespDTO { @@ -22,27 +27,49 @@ public class RewardActivityMatchRespDTO {
22 */ 27 */
23 private String name; 28 private String name;
24 /** 29 /**
  30 + * 状态
  31 + *
  32 + * 枚举 {@link CommonStatusEnum}
  33 + */
  34 + private Integer status;
  35 + /**
  36 + * 开始时间
  37 + */
  38 + private LocalDateTime startTime;
  39 + /**
  40 + * 结束时间
  41 + */
  42 + private LocalDateTime endTime;
  43 + /**
  44 + * 备注
  45 + */
  46 + private String remark;
  47 + /**
25 * 条件类型 48 * 条件类型
26 * 49 *
27 * 枚举 {@link PromotionConditionTypeEnum} 50 * 枚举 {@link PromotionConditionTypeEnum}
28 */ 51 */
29 private Integer conditionType; 52 private Integer conditionType;
30 /** 53 /**
31 - * 优惠规则的数组 54 + * 商品范围
  55 + *
  56 + * 枚举 {@link PromotionProductScopeEnum}
32 */ 57 */
33 - private List<Rule> rules;  
34 - 58 + private Integer productScope;
35 /** 59 /**
36 * 商品 SPU 编号的数组 60 * 商品 SPU 编号的数组
37 */ 61 */
38 - private List<Long> spuIds; 62 + private List<Long> productScopeValues;
  63 + /**
  64 + * 优惠规则的数组
  65 + */
  66 + private List<Rule> rules;
39 67
40 - // TODO 芋艿:后面 RewardActivityRespDTO 有了之后,Rule 可以放过去  
41 /** 68 /**
42 * 优惠规则 69 * 优惠规则
43 */ 70 */
44 @Data 71 @Data
45 - public static class Rule { 72 + public static class Rule implements Serializable {
46 73
47 /** 74 /**
48 * 优惠门槛 75 * 优惠门槛
@@ -64,13 +91,14 @@ public class RewardActivityMatchRespDTO { @@ -64,13 +91,14 @@ public class RewardActivityMatchRespDTO {
64 */ 91 */
65 private Integer point; 92 private Integer point;
66 /** 93 /**
67 - * 赠送的优惠劵编号的数组  
68 - */  
69 - private List<Long> couponIds;  
70 - /**  
71 - * 赠送的优惠券数量的数组 94 + * 赠送的优惠劵
  95 + *
  96 + * key: 优惠劵模版编号
  97 + * value:对应的优惠券数量
  98 + *
  99 + * 目的:用于订单支付后赠送优惠券
72 */ 100 */
73 - private List<Integer> couponCounts; 101 + private Map<Long, Integer> giveCouponTemplateCounts;
74 102
75 } 103 }
76 104
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 { @@ -44,7 +44,8 @@ public interface ErrorCodeConstants {
44 ErrorCode REWARD_ACTIVITY_UPDATE_FAIL_STATUS_CLOSED = new ErrorCode(1_013_006_002, "满减送活动已关闭,不能修改"); 44 ErrorCode REWARD_ACTIVITY_UPDATE_FAIL_STATUS_CLOSED = new ErrorCode(1_013_006_002, "满减送活动已关闭,不能修改");
45 ErrorCode REWARD_ACTIVITY_DELETE_FAIL_STATUS_NOT_CLOSED = new ErrorCode(1_013_006_003, "满减送活动未关闭,不能删除"); 45 ErrorCode REWARD_ACTIVITY_DELETE_FAIL_STATUS_NOT_CLOSED = new ErrorCode(1_013_006_003, "满减送活动未关闭,不能删除");
46 ErrorCode REWARD_ACTIVITY_CLOSE_FAIL_STATUS_CLOSED = new ErrorCode(1_013_006_004, "满减送活动已关闭,不能重复关闭"); 46 ErrorCode REWARD_ACTIVITY_CLOSE_FAIL_STATUS_CLOSED = new ErrorCode(1_013_006_004, "满减送活动已关闭,不能重复关闭");
47 - ErrorCode REWARD_ACTIVITY_CLOSE_FAIL_STATUS_END = new ErrorCode(1_013_006_005, "满减送活动已结束,不能关闭"); 47 + ErrorCode REWARD_ACTIVITY_SCOPE_ALL_EXISTS = new ErrorCode(1_013_006_005, "已存在商品范围为全场的满减送活动");
  48 + ErrorCode REWARD_ACTIVITY_SCOPE_CATEGORY_EXISTS = new ErrorCode(1_013_006_006, "存在商品类型参加了其它满减送活动");
48 49
49 // ========== TODO 空着 1-013-007-000 ============ 50 // ========== TODO 空着 1-013-007-000 ============
50 51
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; @@ -5,6 +5,7 @@ import lombok.AllArgsConstructor;
5 import lombok.Getter; 5 import lombok.Getter;
6 6
7 import java.util.Arrays; 7 import java.util.Arrays;
  8 +import java.util.Objects;
8 9
9 /** 10 /**
10 * 营销的商品范围枚举 11 * 营销的商品范围枚举
@@ -15,10 +16,9 @@ import java.util.Arrays; @@ -15,10 +16,9 @@ import java.util.Arrays;
15 @AllArgsConstructor 16 @AllArgsConstructor
16 public enum PromotionProductScopeEnum implements IntArrayValuable { 17 public enum PromotionProductScopeEnum implements IntArrayValuable {
17 18
18 - ALL(1, "通用券"), // 全部商品  
19 - SPU(2, "商品券"), // 指定商品  
20 - CATEGORY(3, "品类券"), // 指定品类  
21 - ; 19 + ALL(1, "全部商品"),
  20 + SPU(2, "指定商品"),
  21 + CATEGORY(3, "指定品类");
22 22
23 public static final int[] ARRAYS = Arrays.stream(values()).mapToInt(PromotionProductScopeEnum::getScope).toArray(); 23 public static final int[] ARRAYS = Arrays.stream(values()).mapToInt(PromotionProductScopeEnum::getScope).toArray();
24 24
@@ -36,4 +36,16 @@ public enum PromotionProductScopeEnum implements IntArrayValuable { @@ -36,4 +36,16 @@ public enum PromotionProductScopeEnum implements IntArrayValuable {
36 return ARRAYS; 36 return ARRAYS;
37 } 37 }
38 38
  39 + public static boolean isAll(Integer scope) {
  40 + return Objects.equals(scope, ALL.scope);
  41 + }
  42 +
  43 + public static boolean isSpu(Integer scope) {
  44 + return Objects.equals(scope, SPU.scope);
  45 + }
  46 +
  47 + public static boolean isCategory(Integer scope) {
  48 + return Objects.equals(scope, CATEGORY.scope);
  49 + }
  50 +
39 } 51 }
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 { @@ -17,8 +17,7 @@ public enum CouponStatusEnum implements IntArrayValuable {
17 17
18 UNUSED(1, "未使用"), 18 UNUSED(1, "未使用"),
19 USED(2, "已使用"), 19 USED(2, "已使用"),
20 - EXPIRE(3, "已过期"),  
21 - ; 20 + EXPIRE(3, "已过期");
22 21
23 public static final int[] ARRAYS = Arrays.stream(values()).mapToInt(CouponStatusEnum::getStatus).toArray(); 22 public static final int[] ARRAYS = Arrays.stream(values()).mapToInt(CouponStatusEnum::getStatus).toArray();
24 23
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; @@ -7,10 +7,12 @@ import cn.iocoder.yudao.module.promotion.api.coupon.dto.CouponValidReqDTO;
7 import cn.iocoder.yudao.module.promotion.convert.coupon.CouponConvert; 7 import cn.iocoder.yudao.module.promotion.convert.coupon.CouponConvert;
8 import cn.iocoder.yudao.module.promotion.dal.dataobject.coupon.CouponDO; 8 import cn.iocoder.yudao.module.promotion.dal.dataobject.coupon.CouponDO;
9 import cn.iocoder.yudao.module.promotion.service.coupon.CouponService; 9 import cn.iocoder.yudao.module.promotion.service.coupon.CouponService;
  10 +import jakarta.annotation.Resource;
10 import org.springframework.stereotype.Service; 11 import org.springframework.stereotype.Service;
11 import org.springframework.validation.annotation.Validated; 12 import org.springframework.validation.annotation.Validated;
12 13
13 -import jakarta.annotation.Resource; 14 +import java.util.List;
  15 +import java.util.Map;
14 16
15 /** 17 /**
16 * 优惠劵 API 实现类 18 * 优惠劵 API 实现类
@@ -41,4 +43,14 @@ public class CouponApiImpl implements CouponApi { @@ -41,4 +43,14 @@ public class CouponApiImpl implements CouponApi {
41 return CouponConvert.INSTANCE.convert(coupon); 43 return CouponConvert.INSTANCE.convert(coupon);
42 } 44 }
43 45
  46 + @Override
  47 + public List<Long> takeCouponsByAdmin(Map<Long, Integer> giveCoupons, Long userId) {
  48 + return couponService.takeCouponsByAdmin(giveCoupons, userId);
  49 + }
  50 +
  51 + @Override
  52 + public void invalidateCouponsByAdmin(List<Long> giveCouponIds, Long userId) {
  53 + couponService.invalidateCouponsByAdmin(giveCouponIds, userId);
  54 + }
  55 +
44 } 56 }
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; @@ -2,23 +2,22 @@ package cn.iocoder.yudao.module.promotion.controller.admin.reward;
2 2
3 import cn.iocoder.yudao.framework.common.pojo.CommonResult; 3 import cn.iocoder.yudao.framework.common.pojo.CommonResult;
4 import cn.iocoder.yudao.framework.common.pojo.PageResult; 4 import cn.iocoder.yudao.framework.common.pojo.PageResult;
  5 +import cn.iocoder.yudao.framework.common.util.object.BeanUtils;
5 import cn.iocoder.yudao.module.promotion.controller.admin.reward.vo.RewardActivityCreateReqVO; 6 import cn.iocoder.yudao.module.promotion.controller.admin.reward.vo.RewardActivityCreateReqVO;
6 import cn.iocoder.yudao.module.promotion.controller.admin.reward.vo.RewardActivityPageReqVO; 7 import cn.iocoder.yudao.module.promotion.controller.admin.reward.vo.RewardActivityPageReqVO;
7 import cn.iocoder.yudao.module.promotion.controller.admin.reward.vo.RewardActivityRespVO; 8 import cn.iocoder.yudao.module.promotion.controller.admin.reward.vo.RewardActivityRespVO;
8 import cn.iocoder.yudao.module.promotion.controller.admin.reward.vo.RewardActivityUpdateReqVO; 9 import cn.iocoder.yudao.module.promotion.controller.admin.reward.vo.RewardActivityUpdateReqVO;
9 -import cn.iocoder.yudao.module.promotion.convert.reward.RewardActivityConvert;  
10 import cn.iocoder.yudao.module.promotion.dal.dataobject.reward.RewardActivityDO; 10 import cn.iocoder.yudao.module.promotion.dal.dataobject.reward.RewardActivityDO;
11 import cn.iocoder.yudao.module.promotion.service.reward.RewardActivityService; 11 import cn.iocoder.yudao.module.promotion.service.reward.RewardActivityService;
12 import io.swagger.v3.oas.annotations.Operation; 12 import io.swagger.v3.oas.annotations.Operation;
13 import io.swagger.v3.oas.annotations.Parameter; 13 import io.swagger.v3.oas.annotations.Parameter;
14 import io.swagger.v3.oas.annotations.tags.Tag; 14 import io.swagger.v3.oas.annotations.tags.Tag;
  15 +import jakarta.annotation.Resource;
  16 +import jakarta.validation.Valid;
15 import org.springframework.security.access.prepost.PreAuthorize; 17 import org.springframework.security.access.prepost.PreAuthorize;
16 import org.springframework.validation.annotation.Validated; 18 import org.springframework.validation.annotation.Validated;
17 import org.springframework.web.bind.annotation.*; 19 import org.springframework.web.bind.annotation.*;
18 20
19 -import jakarta.annotation.Resource;  
20 -import jakarta.validation.Valid;  
21 -  
22 import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success; 21 import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success;
23 22
24 @Tag(name = "管理后台 - 满减送活动") 23 @Tag(name = "管理后台 - 满减送活动")
@@ -69,7 +68,7 @@ public class RewardActivityController { @@ -69,7 +68,7 @@ public class RewardActivityController {
69 @PreAuthorize("@ss.hasPermission('promotion:reward-activity:query')") 68 @PreAuthorize("@ss.hasPermission('promotion:reward-activity:query')")
70 public CommonResult<RewardActivityRespVO> getRewardActivity(@RequestParam("id") Long id) { 69 public CommonResult<RewardActivityRespVO> getRewardActivity(@RequestParam("id") Long id) {
71 RewardActivityDO rewardActivity = rewardActivityService.getRewardActivity(id); 70 RewardActivityDO rewardActivity = rewardActivityService.getRewardActivity(id);
72 - return success(RewardActivityConvert.INSTANCE.convert(rewardActivity)); 71 + return success(BeanUtils.toBean(rewardActivity, RewardActivityRespVO.class));
73 } 72 }
74 73
75 @GetMapping("/page") 74 @GetMapping("/page")
@@ -77,7 +76,7 @@ public class RewardActivityController { @@ -77,7 +76,7 @@ public class RewardActivityController {
77 @PreAuthorize("@ss.hasPermission('promotion:reward-activity:query')") 76 @PreAuthorize("@ss.hasPermission('promotion:reward-activity:query')")
78 public CommonResult<PageResult<RewardActivityRespVO>> getRewardActivityPage(@Valid RewardActivityPageReqVO pageVO) { 77 public CommonResult<PageResult<RewardActivityRespVO>> getRewardActivityPage(@Valid RewardActivityPageReqVO pageVO) {
79 PageResult<RewardActivityDO> pageResult = rewardActivityService.getRewardActivityPage(pageVO); 78 PageResult<RewardActivityDO> pageResult = rewardActivityService.getRewardActivityPage(pageVO);
80 - return success(RewardActivityConvert.INSTANCE.convertPage(pageResult)); 79 + return success(BeanUtils.toBean(pageResult, RewardActivityRespVO.class));
81 } 80 }
82 81
83 } 82 }
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 @@ -6,18 +6,17 @@ import cn.iocoder.yudao.module.promotion.enums.common.PromotionConditionTypeEnum
6 import cn.iocoder.yudao.module.promotion.enums.common.PromotionProductScopeEnum; 6 import cn.iocoder.yudao.module.promotion.enums.common.PromotionProductScopeEnum;
7 import com.fasterxml.jackson.annotation.JsonIgnore; 7 import com.fasterxml.jackson.annotation.JsonIgnore;
8 import io.swagger.v3.oas.annotations.media.Schema; 8 import io.swagger.v3.oas.annotations.media.Schema;
9 -import lombok.Data;  
10 -import org.springframework.format.annotation.DateTimeFormat;  
11 -  
12 import jakarta.validation.Valid; 9 import jakarta.validation.Valid;
13 import jakarta.validation.constraints.AssertTrue; 10 import jakarta.validation.constraints.AssertTrue;
14 import jakarta.validation.constraints.Future; 11 import jakarta.validation.constraints.Future;
15 import jakarta.validation.constraints.Min; 12 import jakarta.validation.constraints.Min;
16 import jakarta.validation.constraints.NotNull; 13 import jakarta.validation.constraints.NotNull;
  14 +import lombok.Data;
  15 +
17 import java.time.LocalDateTime; 16 import java.time.LocalDateTime;
18 import java.util.List; 17 import java.util.List;
19 -  
20 -import static cn.iocoder.yudao.framework.common.util.date.DateUtils.FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND; 18 +import java.util.Map;
  19 +import java.util.Objects;
21 20
22 /** 21 /**
23 * 满减送活动 Base VO,提供给添加、修改、详细的子 VO 使用 22 * 满减送活动 Base VO,提供给添加、修改、详细的子 VO 使用
@@ -32,12 +31,10 @@ public class RewardActivityBaseVO { @@ -32,12 +31,10 @@ public class RewardActivityBaseVO {
32 31
33 @Schema(description = "开始时间", requiredMode = Schema.RequiredMode.REQUIRED) 32 @Schema(description = "开始时间", requiredMode = Schema.RequiredMode.REQUIRED)
34 @NotNull(message = "开始时间不能为空") 33 @NotNull(message = "开始时间不能为空")
35 - @DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND)  
36 private LocalDateTime startTime; 34 private LocalDateTime startTime;
37 35
38 @Schema(description = "结束时间", requiredMode = Schema.RequiredMode.REQUIRED) 36 @Schema(description = "结束时间", requiredMode = Schema.RequiredMode.REQUIRED)
39 @NotNull(message = "结束时间不能为空") 37 @NotNull(message = "结束时间不能为空")
40 - @DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND)  
41 @Future(message = "结束时间必须大于当前时间") 38 @Future(message = "结束时间必须大于当前时间")
42 private LocalDateTime endTime; 39 private LocalDateTime endTime;
43 40
@@ -54,8 +51,8 @@ public class RewardActivityBaseVO { @@ -54,8 +51,8 @@ public class RewardActivityBaseVO {
54 @InEnum(value = PromotionProductScopeEnum.class, message = "商品范围必须是 {value}") 51 @InEnum(value = PromotionProductScopeEnum.class, message = "商品范围必须是 {value}")
55 private Integer productScope; 52 private Integer productScope;
56 53
57 - @Schema(description = "商品 SPU 编号的数组", example = "1,2,3")  
58 - private List<Long> productSpuIds; 54 + @Schema(description = "商品范围编号的数组", example = "[1, 3]")
  55 + private List<Long> productScopeValues;
59 56
60 /** 57 /**
61 * 优惠规则的数组 58 * 优惠规则的数组
@@ -76,24 +73,28 @@ public class RewardActivityBaseVO { @@ -76,24 +73,28 @@ public class RewardActivityBaseVO {
76 private Integer discountPrice; 73 private Integer discountPrice;
77 74
78 @Schema(description = "是否包邮", requiredMode = Schema.RequiredMode.REQUIRED, example = "true") 75 @Schema(description = "是否包邮", requiredMode = Schema.RequiredMode.REQUIRED, example = "true")
  76 + @NotNull(message = "规则是否包邮不能为空")
79 private Boolean freeDelivery; 77 private Boolean freeDelivery;
80 78
81 @Schema(description = "赠送的积分", requiredMode = Schema.RequiredMode.REQUIRED, example = "100") 79 @Schema(description = "赠送的积分", requiredMode = Schema.RequiredMode.REQUIRED, example = "100")
82 - @Min(value = 1L, message = "赠送的积分必须大于等于 1")  
83 private Integer point; 80 private Integer point;
84 81
85 - @Schema(description = "赠送的优惠劵编号的数组", example = "1,2,3")  
86 - private List<Long> couponIds;  
87 -  
88 - @Schema(description = "赠送的优惠券数量的数组", example = "1,2,3")  
89 - private List<Integer> couponCounts; 82 + @Schema(description = "赠送的优惠劵编号的数组")
  83 + private Map<Long, Integer> giveCouponTemplateCounts;
90 84
91 - @AssertTrue(message = "优惠劵和数量必须一一对应") 85 + @AssertTrue(message = "赠送的积分不能小于 0")
92 @JsonIgnore 86 @JsonIgnore
93 - public boolean isCouponCountsValid() {  
94 - return CollUtil.size(couponCounts) == CollUtil.size(couponCounts); 87 + public boolean isPointValid() {
  88 + return point == null || point >= 0;
95 } 89 }
96 90
97 } 91 }
98 92
  93 + @AssertTrue(message = "商品范围编号的数组不能为空")
  94 + @JsonIgnore
  95 + public boolean isProductScopeValuesValid() {
  96 + return Objects.equals(productScope, PromotionProductScopeEnum.ALL.getScope()) // 全部范围时,可以为空
  97 + || CollUtil.isNotEmpty(productScopeValues);
  98 + }
  99 +
99 } 100 }
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; @@ -2,8 +2,11 @@ package cn.iocoder.yudao.module.promotion.controller.app.activity;
2 2
3 import cn.hutool.core.collection.CollUtil; 3 import cn.hutool.core.collection.CollUtil;
4 import cn.hutool.core.map.MapUtil; 4 import cn.hutool.core.map.MapUtil;
  5 +import cn.hutool.core.util.ObjUtil;
5 import cn.iocoder.yudao.framework.common.enums.CommonStatusEnum; 6 import cn.iocoder.yudao.framework.common.enums.CommonStatusEnum;
6 import cn.iocoder.yudao.framework.common.pojo.CommonResult; 7 import cn.iocoder.yudao.framework.common.pojo.CommonResult;
  8 +import cn.iocoder.yudao.module.product.api.spu.ProductSpuApi;
  9 +import cn.iocoder.yudao.module.product.api.spu.dto.ProductSpuRespDTO;
7 import cn.iocoder.yudao.module.promotion.controller.app.activity.vo.AppActivityRespVO; 10 import cn.iocoder.yudao.module.promotion.controller.app.activity.vo.AppActivityRespVO;
8 import cn.iocoder.yudao.module.promotion.dal.dataobject.bargain.BargainActivityDO; 11 import cn.iocoder.yudao.module.promotion.dal.dataobject.bargain.BargainActivityDO;
9 import cn.iocoder.yudao.module.promotion.dal.dataobject.combination.CombinationActivityDO; 12 import cn.iocoder.yudao.module.promotion.dal.dataobject.combination.CombinationActivityDO;
@@ -11,7 +14,7 @@ import cn.iocoder.yudao.module.promotion.dal.dataobject.discount.DiscountActivit @@ -11,7 +14,7 @@ import cn.iocoder.yudao.module.promotion.dal.dataobject.discount.DiscountActivit
11 import cn.iocoder.yudao.module.promotion.dal.dataobject.discount.DiscountProductDO; 14 import cn.iocoder.yudao.module.promotion.dal.dataobject.discount.DiscountProductDO;
12 import cn.iocoder.yudao.module.promotion.dal.dataobject.reward.RewardActivityDO; 15 import cn.iocoder.yudao.module.promotion.dal.dataobject.reward.RewardActivityDO;
13 import cn.iocoder.yudao.module.promotion.dal.dataobject.seckill.SeckillActivityDO; 16 import cn.iocoder.yudao.module.promotion.dal.dataobject.seckill.SeckillActivityDO;
14 -import cn.iocoder.yudao.module.promotion.enums.common.PromotionActivityStatusEnum; 17 +import cn.iocoder.yudao.module.promotion.enums.common.PromotionProductScopeEnum;
15 import cn.iocoder.yudao.module.promotion.enums.common.PromotionTypeEnum; 18 import cn.iocoder.yudao.module.promotion.enums.common.PromotionTypeEnum;
16 import cn.iocoder.yudao.module.promotion.service.bargain.BargainActivityService; 19 import cn.iocoder.yudao.module.promotion.service.bargain.BargainActivityService;
17 import cn.iocoder.yudao.module.promotion.service.combination.CombinationActivityService; 20 import cn.iocoder.yudao.module.promotion.service.combination.CombinationActivityService;
@@ -30,7 +33,6 @@ import org.springframework.web.bind.annotation.RestController; @@ -30,7 +33,6 @@ import org.springframework.web.bind.annotation.RestController;
30 33
31 import java.time.LocalDateTime; 34 import java.time.LocalDateTime;
32 import java.util.*; 35 import java.util.*;
33 -import java.util.stream.Collectors;  
34 36
35 import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success; 37 import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success;
36 import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.*; 38 import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.*;
@@ -52,6 +54,9 @@ public class AppActivityController { @@ -52,6 +54,9 @@ public class AppActivityController {
52 @Resource 54 @Resource
53 private RewardActivityService rewardActivityService; 55 private RewardActivityService rewardActivityService;
54 56
  57 + @Resource
  58 + private ProductSpuApi productSpuApi;
  59 +
55 @GetMapping("/list-by-spu-id") 60 @GetMapping("/list-by-spu-id")
56 @Operation(summary = "获得单个商品,近期参与的每个活动") 61 @Operation(summary = "获得单个商品,近期参与的每个活动")
57 @Parameter(name = "spuId", description = "商品编号", required = true) 62 @Parameter(name = "spuId", description = "商品编号", required = true)
@@ -87,7 +92,7 @@ public class AppActivityController { @@ -87,7 +92,7 @@ public class AppActivityController {
87 // 4. 限时折扣活动 92 // 4. 限时折扣活动
88 getDiscountActivities(spuIds, now, activityList); 93 getDiscountActivities(spuIds, now, activityList);
89 // 5. 满减送活动 94 // 5. 满减送活动
90 - getRewardActivities(spuIds, now, activityList); 95 + getRewardActivityList(spuIds, now, activityList);
91 return activityList; 96 return activityList;
92 } 97 }
93 98
@@ -144,28 +149,51 @@ public class AppActivityController { @@ -144,28 +149,51 @@ public class AppActivityController {
144 item.getName(), productMap.get(item.getId()), item.getStartTime(), item.getEndTime()))); 149 item.getName(), productMap.get(item.getId()), item.getStartTime(), item.getEndTime())));
145 } 150 }
146 151
147 - private void getRewardActivities(Collection<Long> spuIds, LocalDateTime now, List<AppActivityRespVO> activityList) {  
148 - // TODO @puhui999:有 3 范围,不只 spuId,还有 categoryId,全部  
149 - List<RewardActivityDO> rewardActivityList = rewardActivityService.getRewardActivityBySpuIdsAndStatusAndDateTimeLt(  
150 - spuIds, PromotionActivityStatusEnum.RUN.getStatus(), now); 152 + private void getRewardActivityList(Collection<Long> spuIds, LocalDateTime now, List<AppActivityRespVO> activityList) {
  153 + // 1.1 获得所有的活动
  154 + List<RewardActivityDO> rewardActivityList = rewardActivityService.getRewardActivityListByStatusAndDateTimeLt(
  155 + CommonStatusEnum.ENABLE.getStatus(), now);
151 if (CollUtil.isEmpty(rewardActivityList)) { 156 if (CollUtil.isEmpty(rewardActivityList)) {
152 return; 157 return;
153 } 158 }
  159 + // 1.2 获得所有的商品信息
  160 + List<ProductSpuRespDTO> spuList = productSpuApi.getSpuList(spuIds);
  161 + if (CollUtil.isEmpty(spuList)) {
  162 + return;
  163 + }
154 164
155 - Map<Long, Optional<RewardActivityDO>> spuIdAndActivityMap = spuIds.stream()  
156 - .collect(Collectors.toMap(  
157 - spuId -> spuId,  
158 - spuId -> rewardActivityList.stream()  
159 - .filter(activity -> activity.getProductSpuIds().contains(spuId))  
160 - .max(Comparator.comparing(RewardActivityDO::getCreateTime))));  
161 - for (Long supId : spuIdAndActivityMap.keySet()) {  
162 - if (spuIdAndActivityMap.get(supId).isEmpty()) {  
163 - continue; 165 + // 2. 构建活动
  166 + for (RewardActivityDO rewardActivity : rewardActivityList) {
  167 + // 情况一:所有商品都能参加
  168 + if (PromotionProductScopeEnum.isAll(rewardActivity.getProductScope())) {
  169 + buildAppActivityRespVO(rewardActivity, spuIds, activityList);
  170 + }
  171 + // 情况二:指定商品参加
  172 + if (PromotionProductScopeEnum.isSpu(rewardActivity.getProductScope())) {
  173 + List<Long> fSpuIds = spuList.stream().map(ProductSpuRespDTO::getId).filter(id ->
  174 + rewardActivity.getProductScopeValues().contains(id)).toList();
  175 + buildAppActivityRespVO(rewardActivity, fSpuIds, activityList);
164 } 176 }
  177 + // 情况三:指定商品类型参加
  178 + if (PromotionProductScopeEnum.isCategory(rewardActivity.getProductScope())) {
  179 + List<Long> fSpuIds = spuList.stream().filter(spuItem -> rewardActivity.getProductScopeValues()
  180 + .contains(spuItem.getCategoryId())).map(ProductSpuRespDTO::getId).toList();
  181 + buildAppActivityRespVO(rewardActivity, fSpuIds, activityList);
  182 + }
  183 + }
  184 + }
165 185
166 - RewardActivityDO rewardActivityDO = spuIdAndActivityMap.get(supId).get();  
167 - activityList.add(new AppActivityRespVO(rewardActivityDO.getId(), PromotionTypeEnum.REWARD_ACTIVITY.getType(),  
168 - rewardActivityDO.getName(), supId, rewardActivityDO.getStartTime(), rewardActivityDO.getEndTime())); 186 + private static void buildAppActivityRespVO(RewardActivityDO rewardActivity, Collection<Long> spuIds,
  187 + List<AppActivityRespVO> activityList) {
  188 + for (Long spuId : spuIds) {
  189 + // 校验商品是否已经加入过活动
  190 + if (anyMatch(activityList, appActivity -> ObjUtil.equal(appActivity.getId(), rewardActivity.getId()) &&
  191 + ObjUtil.equal(appActivity.getSpuId(), spuId))) {
  192 + continue;
  193 + }
  194 + activityList.add(new AppActivityRespVO(rewardActivity.getId(),
  195 + PromotionTypeEnum.REWARD_ACTIVITY.getType(), rewardActivity.getName(), spuId,
  196 + rewardActivity.getStartTime(), rewardActivity.getEndTime()));
169 } 197 }
170 } 198 }
171 199
yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/convert/reward/RewardActivityConvert.java deleted
1 -package cn.iocoder.yudao.module.promotion.convert.reward;  
2 -  
3 -import cn.iocoder.yudao.framework.common.pojo.PageResult;  
4 -import cn.iocoder.yudao.module.promotion.controller.admin.reward.vo.RewardActivityCreateReqVO;  
5 -import cn.iocoder.yudao.module.promotion.controller.admin.reward.vo.RewardActivityRespVO;  
6 -import cn.iocoder.yudao.module.promotion.controller.admin.reward.vo.RewardActivityUpdateReqVO;  
7 -import cn.iocoder.yudao.module.promotion.dal.dataobject.reward.RewardActivityDO;  
8 -import org.mapstruct.Mapper;  
9 -import org.mapstruct.factory.Mappers;  
10 -  
11 -/**  
12 - * 满减送活动 Convert  
13 - *  
14 - * @author 芋道源码  
15 - */  
16 -@Mapper  
17 -public interface RewardActivityConvert {  
18 -  
19 - RewardActivityConvert INSTANCE = Mappers.getMapper(RewardActivityConvert.class);  
20 -  
21 - RewardActivityDO convert(RewardActivityCreateReqVO bean);  
22 -  
23 - RewardActivityDO convert(RewardActivityUpdateReqVO bean);  
24 -  
25 - RewardActivityRespVO convert(RewardActivityDO bean);  
26 -  
27 - PageResult<RewardActivityRespVO> convertPage(PageResult<RewardActivityDO> page);  
28 -  
29 -}  
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 { @@ -50,7 +50,6 @@ public class CouponDO extends BaseDO {
50 * 50 *
51 * 枚举 {@link CouponStatusEnum} 51 * 枚举 {@link CouponStatusEnum}
52 */ 52 */
53 - // TODO 芋艿:已作废?  
54 private Integer status; 53 private Integer status;
55 54
56 // TODO 芋艿:发放 adminid? 55 // TODO 芋艿:发放 adminid?
yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/dal/dataobject/reward/RewardActivityDO.java
1 package cn.iocoder.yudao.module.promotion.dal.dataobject.reward; 1 package cn.iocoder.yudao.module.promotion.dal.dataobject.reward;
2 2
  3 +import cn.iocoder.yudao.framework.common.enums.CommonStatusEnum;
3 import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO; 4 import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO;
4 import cn.iocoder.yudao.framework.mybatis.core.type.LongListTypeHandler; 5 import cn.iocoder.yudao.framework.mybatis.core.type.LongListTypeHandler;
5 -import cn.iocoder.yudao.module.promotion.enums.common.PromotionActivityStatusEnum;  
6 import cn.iocoder.yudao.module.promotion.enums.common.PromotionConditionTypeEnum; 6 import cn.iocoder.yudao.module.promotion.enums.common.PromotionConditionTypeEnum;
7 import cn.iocoder.yudao.module.promotion.enums.common.PromotionProductScopeEnum; 7 import cn.iocoder.yudao.module.promotion.enums.common.PromotionProductScopeEnum;
8 import com.baomidou.mybatisplus.annotation.KeySequence; 8 import com.baomidou.mybatisplus.annotation.KeySequence;
@@ -16,6 +16,7 @@ import lombok.EqualsAndHashCode; @@ -16,6 +16,7 @@ import lombok.EqualsAndHashCode;
16 import java.io.Serializable; 16 import java.io.Serializable;
17 import java.time.LocalDateTime; 17 import java.time.LocalDateTime;
18 import java.util.List; 18 import java.util.List;
  19 +import java.util.Map;
19 20
20 /** 21 /**
21 * 满减送活动 DO 22 * 满减送活动 DO
@@ -40,7 +41,7 @@ public class RewardActivityDO extends BaseDO { @@ -40,7 +41,7 @@ public class RewardActivityDO extends BaseDO {
40 /** 41 /**
41 * 状态 42 * 状态
42 * 43 *
43 - * 枚举 {@link PromotionActivityStatusEnum} 44 + * 枚举 {@link CommonStatusEnum}
44 */ 45 */
45 private Integer status; 46 private Integer status;
46 /** 47 /**
@@ -71,7 +72,7 @@ public class RewardActivityDO extends BaseDO { @@ -71,7 +72,7 @@ public class RewardActivityDO extends BaseDO {
71 * 商品 SPU 编号的数组 72 * 商品 SPU 编号的数组
72 */ 73 */
73 @TableField(typeHandler = LongListTypeHandler.class) 74 @TableField(typeHandler = LongListTypeHandler.class)
74 - private List<Long> productSpuIds; 75 + private List<Long> productScopeValues;
75 /** 76 /**
76 * 优惠规则的数组 77 * 优惠规则的数组
77 */ 78 */
@@ -104,13 +105,14 @@ public class RewardActivityDO extends BaseDO { @@ -104,13 +105,14 @@ public class RewardActivityDO extends BaseDO {
104 */ 105 */
105 private Integer point; 106 private Integer point;
106 /** 107 /**
107 - * 赠送的优惠劵编号的数组  
108 - */  
109 - private List<Long> couponIds;  
110 - /**  
111 - * 赠送的优惠券数量的数组 108 + * 赠送的优惠劵
  109 + *
  110 + * key: 优惠劵模版编号
  111 + * value:对应的优惠券数量
  112 + *
  113 + * 目的:用于订单支付后赠送优惠券
112 */ 114 */
113 - private List<Integer> couponCounts; 115 + private Map<Long, Integer> giveCouponTemplateCounts;
114 116
115 } 117 }
116 118
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&lt;RewardActivityDO&gt; { @@ -30,10 +30,6 @@ public interface RewardActivityMapper extends BaseMapperX&lt;RewardActivityDO&gt; {
30 .orderByDesc(RewardActivityDO::getId)); 30 .orderByDesc(RewardActivityDO::getId));
31 } 31 }
32 32
33 - default List<RewardActivityDO> selectListByStatus(Collection<Integer> statuses) {  
34 - return selectList(RewardActivityDO::getStatus, statuses);  
35 - }  
36 -  
37 default List<RewardActivityDO> selectListByProductScopeAndStatus(Integer productScope, Integer status) { 33 default List<RewardActivityDO> selectListByProductScopeAndStatus(Integer productScope, Integer status) {
38 return selectList(new LambdaQueryWrapperX<RewardActivityDO>() 34 return selectList(new LambdaQueryWrapperX<RewardActivityDO>()
39 .eq(RewardActivityDO::getProductScope, productScope) 35 .eq(RewardActivityDO::getProductScope, productScope)
@@ -53,16 +49,16 @@ public interface RewardActivityMapper extends BaseMapperX&lt;RewardActivityDO&gt; { @@ -53,16 +49,16 @@ public interface RewardActivityMapper extends BaseMapperX&lt;RewardActivityDO&gt; {
53 * 获取指定活动编号的活动列表且 49 * 获取指定活动编号的活动列表且
54 * 开始时间和结束时间小于给定时间 dateTime 的活动列表 50 * 开始时间和结束时间小于给定时间 dateTime 的活动列表
55 * 51 *
56 - * @param ids 活动编号 52 + * @param status 状态
57 * @param dateTime 指定日期 53 * @param dateTime 指定日期
58 * @return 活动列表 54 * @return 活动列表
59 */ 55 */
60 - default List<RewardActivityDO> selectListByIdsAndDateTimeLt(Collection<Long> ids, LocalDateTime dateTime) { 56 + default List<RewardActivityDO> selectListByStatusAndDateTimeLt(Integer status, LocalDateTime dateTime) {
61 return selectList(new LambdaQueryWrapperX<RewardActivityDO>() 57 return selectList(new LambdaQueryWrapperX<RewardActivityDO>()
62 - .in(RewardActivityDO::getId, ids) 58 + .eq(RewardActivityDO::getStatus, status)
63 .lt(RewardActivityDO::getStartTime, dateTime) 59 .lt(RewardActivityDO::getStartTime, dateTime)
64 .gt(RewardActivityDO::getEndTime, dateTime)// 开始时间 < 指定时间 < 结束时间,也就是说获取指定时间段的活动 60 .gt(RewardActivityDO::getEndTime, dateTime)// 开始时间 < 指定时间 < 结束时间,也就是说获取指定时间段的活动
65 - .orderByDesc(RewardActivityDO::getCreateTime) 61 + .orderByAsc(RewardActivityDO::getStartTime)
66 ); 62 );
67 } 63 }
68 64
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 @@ -27,6 +27,7 @@ import cn.iocoder.yudao.module.promotion.enums.combination.CombinationRecordStat
27 import cn.iocoder.yudao.module.system.api.social.SocialClientApi; 27 import cn.iocoder.yudao.module.system.api.social.SocialClientApi;
28 import cn.iocoder.yudao.module.system.api.social.dto.SocialWxaSubscribeMessageSendReqDTO; 28 import cn.iocoder.yudao.module.system.api.social.dto.SocialWxaSubscribeMessageSendReqDTO;
29 import cn.iocoder.yudao.module.trade.api.order.TradeOrderApi; 29 import cn.iocoder.yudao.module.trade.api.order.TradeOrderApi;
  30 +import cn.iocoder.yudao.module.trade.enums.order.TradeOrderCancelTypeEnum;
30 import jakarta.annotation.Nullable; 31 import jakarta.annotation.Nullable;
31 import jakarta.annotation.Resource; 32 import jakarta.annotation.Resource;
32 import lombok.extern.slf4j.Slf4j; 33 import lombok.extern.slf4j.Slf4j;
@@ -37,7 +38,10 @@ import org.springframework.transaction.annotation.Transactional; @@ -37,7 +38,10 @@ import org.springframework.transaction.annotation.Transactional;
37 import org.springframework.validation.annotation.Validated; 38 import org.springframework.validation.annotation.Validated;
38 39
39 import java.time.LocalDateTime; 40 import java.time.LocalDateTime;
40 -import java.util.*; 41 +import java.util.ArrayList;
  42 +import java.util.Collection;
  43 +import java.util.List;
  44 +import java.util.Map;
41 45
42 import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception; 46 import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception;
43 import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.*; 47 import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.*;
@@ -335,7 +339,8 @@ public class CombinationRecordServiceImpl implements CombinationRecordService { @@ -335,7 +339,8 @@ public class CombinationRecordServiceImpl implements CombinationRecordService {
335 List<CombinationRecordDO> headAndRecords = updateBatchCombinationRecords(headRecord, 339 List<CombinationRecordDO> headAndRecords = updateBatchCombinationRecords(headRecord,
336 CombinationRecordStatusEnum.FAILED); 340 CombinationRecordStatusEnum.FAILED);
337 // 2. 订单取消 341 // 2. 订单取消
338 - headAndRecords.forEach(item -> tradeOrderApi.cancelPaidOrder(item.getUserId(), item.getOrderId())); 342 + headAndRecords.forEach(item -> tradeOrderApi.cancelPaidOrder(item.getUserId(), item.getOrderId(),
  343 + TradeOrderCancelTypeEnum.COMBINATION_CLOSE.getType()));
339 } 344 }
340 345
341 /** 346 /**
yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/service/coupon/CouponService.java
@@ -39,14 +39,6 @@ public interface CouponService { @@ -39,14 +39,6 @@ public interface CouponService {
39 void validCoupon(CouponDO coupon); 39 void validCoupon(CouponDO coupon);
40 40
41 /** 41 /**
42 - * 获得优惠劵分页  
43 - *  
44 - * @param pageReqVO 分页查询  
45 - * @return 优惠劵分页  
46 - */  
47 - PageResult<CouponDO> getCouponPage(CouponPageReqVO pageReqVO);  
48 -  
49 - /**  
50 * 使用优惠劵 42 * 使用优惠劵
51 * 43 *
52 * @param id 优惠劵编号 44 * @param id 优惠劵编号
@@ -70,42 +62,44 @@ public interface CouponService { @@ -70,42 +62,44 @@ public interface CouponService {
70 void deleteCoupon(Long id); 62 void deleteCoupon(Long id);
71 63
72 /** 64 /**
73 - * 获得用户的优惠劵列表  
74 - *  
75 - * @param userId 用户编号  
76 - * @param status 优惠劵状态  
77 - * @return 优惠劵列表  
78 - */  
79 - List<CouponDO> getCouponList(Long userId, Integer status);  
80 -  
81 - /**  
82 - * 获得未使用的优惠劵数量  
83 - *  
84 - * @param userId 用户编号  
85 - * @return 未使用的优惠劵数量  
86 - */  
87 - Long getUnusedCouponCount(Long userId);  
88 -  
89 - /**  
90 * 领取优惠券 65 * 领取优惠券
91 * 66 *
92 * @param templateId 优惠券模板编号 67 * @param templateId 优惠券模板编号
93 * @param userIds 用户编号列表 68 * @param userIds 用户编号列表
94 * @param takeType 领取方式 69 * @param takeType 领取方式
  70 + * @return key: userId, value: 优惠券编号列表
95 */ 71 */
96 - void takeCoupon(Long templateId, Set<Long> userIds, CouponTakeTypeEnum takeType); 72 + Map<Long, List<Long>> takeCoupon(Long templateId, Set<Long> userIds, CouponTakeTypeEnum takeType);
97 73
98 /** 74 /**
99 * 【管理员】给用户发送优惠券 75 * 【管理员】给用户发送优惠券
100 * 76 *
101 * @param templateId 优惠券模板编号 77 * @param templateId 优惠券模板编号
102 * @param userIds 用户编号列表 78 * @param userIds 用户编号列表
  79 + * @return key: userId, value: 优惠券编号列表
103 */ 80 */
104 - default void takeCouponByAdmin(Long templateId, Set<Long> userIds) {  
105 - takeCoupon(templateId, userIds, CouponTakeTypeEnum.ADMIN); 81 + default Map<Long, List<Long>> takeCouponByAdmin(Long templateId, Set<Long> userIds) {
  82 + return takeCoupon(templateId, userIds, CouponTakeTypeEnum.ADMIN);
106 } 83 }
107 84
108 /** 85 /**
  86 + * 【管理员】给指定用户批量发送优惠券
  87 + *
  88 + * @param giveCoupons key: 优惠劵模版编号,value:对应的数量
  89 + * @param userId 用户编号
  90 + * @return 优惠券编号列表
  91 + */
  92 + List<Long> takeCouponsByAdmin(Map<Long, Integer> giveCoupons, Long userId);
  93 +
  94 + /**
  95 + * 【管理员】作废指定用户的指定优惠劵
  96 + *
  97 + * @param giveCouponIds 赠送的优惠券编号
  98 + * @param userId 用户编号
  99 + */
  100 + void invalidateCouponsByAdmin(List<Long> giveCouponIds, Long userId);
  101 +
  102 + /**
109 * 【会员】领取优惠券 103 * 【会员】领取优惠券
110 * 104 *
111 * @param templateId 优惠券模板编号 105 * @param templateId 优惠券模板编号
@@ -123,16 +117,38 @@ public interface CouponService { @@ -123,16 +117,38 @@ public interface CouponService {
123 void takeCouponByRegister(Long userId); 117 void takeCouponByRegister(Long userId);
124 118
125 /** 119 /**
126 - * 获取会员领取指定优惠券的数量 120 + * 过期优惠券
127 * 121 *
128 - * @param templateId 优惠券模板编号  
129 - * @param userId 用户编号  
130 - * @return 领取优惠券的数量 122 + * @return 过期数量
131 */ 123 */
132 - default Integer getTakeCount(Long templateId, Long userId) {  
133 - Map<Long, Integer> map = getTakeCountMapByTemplateIds(Collections.singleton(templateId), userId);  
134 - return MapUtil.getInt(map, templateId, 0);  
135 - } 124 + int expireCoupon();
  125 +
  126 + // ======================= 查询相关 =======================
  127 +
  128 + /**
  129 + * 获得未使用的优惠劵数量
  130 + *
  131 + * @param userId 用户编号
  132 + * @return 未使用的优惠劵数量
  133 + */
  134 + Long getUnusedCouponCount(Long userId);
  135 +
  136 + /**
  137 + * 获得优惠劵分页
  138 + *
  139 + * @param pageReqVO 分页查询
  140 + * @return 优惠劵分页
  141 + */
  142 + PageResult<CouponDO> getCouponPage(CouponPageReqVO pageReqVO);
  143 +
  144 + /**
  145 + * 获得用户的优惠劵列表
  146 + *
  147 + * @param userId 用户编号
  148 + * @param status 优惠劵状态
  149 + * @return 优惠劵列表
  150 + */
  151 + List<CouponDO> getCouponList(Long userId, Integer status);
136 152
137 /** 153 /**
138 * 统计会员领取优惠券的数量 154 * 统计会员领取优惠券的数量
@@ -144,20 +160,25 @@ public interface CouponService { @@ -144,20 +160,25 @@ public interface CouponService {
144 Map<Long, Integer> getTakeCountMapByTemplateIds(Collection<Long> templateIds, Long userId); 160 Map<Long, Integer> getTakeCountMapByTemplateIds(Collection<Long> templateIds, Long userId);
145 161
146 /** 162 /**
147 - * 获取用户匹配的优惠券列表 163 + * 获取会员领取指定优惠券的数量
148 * 164 *
  165 + * @param templateId 优惠券模板编号
149 * @param userId 用户编号 166 * @param userId 用户编号
150 - * @param matchReqVO 匹配参数  
151 - * @return 优惠券列表 167 + * @return 领取优惠券的数量
152 */ 168 */
153 - List<CouponDO> getMatchCouponList(Long userId, AppCouponMatchReqVO matchReqVO); 169 + default Integer getTakeCount(Long templateId, Long userId) {
  170 + Map<Long, Integer> map = getTakeCountMapByTemplateIds(Collections.singleton(templateId), userId);
  171 + return MapUtil.getInt(map, templateId, 0);
  172 + }
154 173
155 /** 174 /**
156 - * 过期优惠券 175 + * 获取用户匹配的优惠券列表
157 * 176 *
158 - * @return 过期数量 177 + * @param userId 用户编号
  178 + * @param matchReqVO 匹配参数
  179 + * @return 优惠券列表
159 */ 180 */
160 - int expireCoupon(); 181 + List<CouponDO> getMatchCouponList(Long userId, AppCouponMatchReqVO matchReqVO);
161 182
162 /** 183 /**
163 * 获取用户是否可以领取优惠券 184 * 获取用户是否可以领取优惠券
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; @@ -3,6 +3,7 @@ package cn.iocoder.yudao.module.promotion.service.coupon;
3 import cn.hutool.core.collection.CollStreamUtil; 3 import cn.hutool.core.collection.CollStreamUtil;
4 import cn.hutool.core.collection.CollUtil; 4 import cn.hutool.core.collection.CollUtil;
5 import cn.hutool.core.map.MapUtil; 5 import cn.hutool.core.map.MapUtil;
  6 +import cn.hutool.core.util.ObjUtil;
6 import cn.hutool.core.util.ObjectUtil; 7 import cn.hutool.core.util.ObjectUtil;
7 import cn.hutool.core.util.StrUtil; 8 import cn.hutool.core.util.StrUtil;
8 import cn.hutool.extra.spring.SpringUtil; 9 import cn.hutool.extra.spring.SpringUtil;
@@ -19,19 +20,19 @@ import cn.iocoder.yudao.module.promotion.dal.mysql.coupon.CouponMapper; @@ -19,19 +20,19 @@ import cn.iocoder.yudao.module.promotion.dal.mysql.coupon.CouponMapper;
19 import cn.iocoder.yudao.module.promotion.enums.coupon.CouponStatusEnum; 20 import cn.iocoder.yudao.module.promotion.enums.coupon.CouponStatusEnum;
20 import cn.iocoder.yudao.module.promotion.enums.coupon.CouponTakeTypeEnum; 21 import cn.iocoder.yudao.module.promotion.enums.coupon.CouponTakeTypeEnum;
21 import cn.iocoder.yudao.module.promotion.enums.coupon.CouponTemplateValidityTypeEnum; 22 import cn.iocoder.yudao.module.promotion.enums.coupon.CouponTemplateValidityTypeEnum;
  23 +import jakarta.annotation.Resource;
22 import lombok.extern.slf4j.Slf4j; 24 import lombok.extern.slf4j.Slf4j;
23 import org.springframework.stereotype.Service; 25 import org.springframework.stereotype.Service;
24 import org.springframework.transaction.annotation.Transactional; 26 import org.springframework.transaction.annotation.Transactional;
25 import org.springframework.validation.annotation.Validated; 27 import org.springframework.validation.annotation.Validated;
26 28
27 -import jakarta.annotation.Resource;  
28 -  
29 import java.time.LocalDateTime; 29 import java.time.LocalDateTime;
30 import java.util.*; 30 import java.util.*;
31 import java.util.stream.Collectors; 31 import java.util.stream.Collectors;
32 32
33 import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception; 33 import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception;
34 import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.*; 34 import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.*;
  35 +import static cn.iocoder.yudao.framework.common.util.collection.MapUtils.findAndThen;
35 import static cn.iocoder.yudao.module.promotion.enums.ErrorCodeConstants.*; 36 import static cn.iocoder.yudao.module.promotion.enums.ErrorCodeConstants.*;
36 import static java.util.Arrays.asList; 37 import static java.util.Arrays.asList;
37 38
@@ -77,20 +78,6 @@ public class CouponServiceImpl implements CouponService { @@ -77,20 +78,6 @@ public class CouponServiceImpl implements CouponService {
77 } 78 }
78 79
79 @Override 80 @Override
80 - public PageResult<CouponDO> getCouponPage(CouponPageReqVO pageReqVO) {  
81 - // 获得用户编号  
82 - if (StrUtil.isNotEmpty(pageReqVO.getNickname())) {  
83 - List<MemberUserRespDTO> users = memberUserApi.getUserListByNickname(pageReqVO.getNickname());  
84 - if (CollUtil.isEmpty(users)) {  
85 - return PageResult.empty();  
86 - }  
87 - pageReqVO.setUserIds(convertSet(users, MemberUserRespDTO::getId));  
88 - }  
89 - // 分页查询  
90 - return couponMapper.selectPage(pageReqVO);  
91 - }  
92 -  
93 - @Override  
94 public void useCoupon(Long id, Long userId, Long orderId) { 81 public void useCoupon(Long id, Long userId, Long orderId) {
95 // 校验优惠劵 82 // 校验优惠劵
96 validCoupon(id, userId); 83 validCoupon(id, userId);
@@ -147,25 +134,8 @@ public class CouponServiceImpl implements CouponService { @@ -147,25 +134,8 @@ public class CouponServiceImpl implements CouponService {
147 } 134 }
148 135
149 @Override 136 @Override
150 - public List<CouponDO> getCouponList(Long userId, Integer status) {  
151 - return couponMapper.selectListByUserIdAndStatus(userId, status);  
152 - }  
153 -  
154 - private CouponDO validateCouponExists(Long id) {  
155 - CouponDO coupon = couponMapper.selectById(id);  
156 - if (coupon == null) {  
157 - throw exception(COUPON_NOT_EXISTS);  
158 - }  
159 - return coupon;  
160 - }  
161 -  
162 - @Override  
163 - public Long getUnusedCouponCount(Long userId) {  
164 - return couponMapper.selectCountByUserIdAndStatus(userId, CouponStatusEnum.UNUSED.getStatus());  
165 - }  
166 -  
167 - @Override  
168 - public void takeCoupon(Long templateId, Set<Long> userIds, CouponTakeTypeEnum takeType) { 137 + @Transactional(rollbackFor = Exception.class)
  138 + public Map<Long, List<Long>> takeCoupon(Long templateId, Set<Long> userIds, CouponTakeTypeEnum takeType) {
169 CouponTemplateDO template = couponTemplateService.getCouponTemplate(templateId); 139 CouponTemplateDO template = couponTemplateService.getCouponTemplate(templateId);
170 // 1. 过滤掉达到领取限制的用户 140 // 1. 过滤掉达到领取限制的用户
171 removeTakeLimitUser(userIds, template); 141 removeTakeLimitUser(userIds, template);
@@ -173,37 +143,86 @@ public class CouponServiceImpl implements CouponService { @@ -173,37 +143,86 @@ public class CouponServiceImpl implements CouponService {
173 validateCouponTemplateCanTake(template, userIds, takeType); 143 validateCouponTemplateCanTake(template, userIds, takeType);
174 144
175 // 3. 批量保存优惠劵 145 // 3. 批量保存优惠劵
176 - couponMapper.insertBatch(convertList(userIds, userId -> CouponConvert.INSTANCE.convert(template, userId))); 146 + List<CouponDO> couponList = convertList(userIds, userId -> CouponConvert.INSTANCE.convert(template, userId));
  147 + couponMapper.insertBatch(couponList);
177 148
178 - // 3. 增加优惠劵模板的领取数量 149 + // 4. 增加优惠劵模板的领取数量
179 couponTemplateService.updateCouponTemplateTakeCount(templateId, userIds.size()); 150 couponTemplateService.updateCouponTemplateTakeCount(templateId, userIds.size());
  151 +
  152 + return convertMultiMap(couponList, CouponDO::getUserId, CouponDO::getId);
180 } 153 }
181 154
182 @Override 155 @Override
183 - @Transactional(rollbackFor = Exception.class)  
184 - public void takeCouponByRegister(Long userId) {  
185 - List<CouponTemplateDO> templates = couponTemplateService.getCouponTemplateListByTakeType(CouponTakeTypeEnum.REGISTER);  
186 - for (CouponTemplateDO template : templates) {  
187 - takeCoupon(template.getId(), CollUtil.newHashSet(userId), CouponTakeTypeEnum.REGISTER); 156 + public List<Long> takeCouponsByAdmin(Map<Long, Integer> giveCoupons, Long userId) {
  157 + if (CollUtil.isEmpty(giveCoupons)) {
  158 + return Collections.emptyList();
188 } 159 }
  160 +
  161 + List<Long> couponIds = new ArrayList<>();
  162 + // 循环发放
  163 + for (Map.Entry<Long, Integer> entry : giveCoupons.entrySet()) {
  164 + try {
  165 + for (int i = 0; i < entry.getValue(); i++) {
  166 + Map<Long, List<Long>> userCouponIdsMap = getSelf().takeCoupon(entry.getKey(), CollUtil.newHashSet(userId),
  167 + CouponTakeTypeEnum.ADMIN);
  168 + findAndThen(userCouponIdsMap, userId, couponIds::addAll);
  169 + }
  170 + } catch (Exception e) {
  171 + log.error("[takeCouponsByAdmin][coupon({}) 优惠券发放失败]", entry, e);
  172 + }
  173 + }
  174 + return couponIds;
189 } 175 }
190 176
191 @Override 177 @Override
192 - public Map<Long, Integer> getTakeCountMapByTemplateIds(Collection<Long> templateIds, Long userId) {  
193 - if (CollUtil.isEmpty(templateIds)) {  
194 - return Collections.emptyMap(); 178 + public void invalidateCouponsByAdmin(List<Long> giveCouponIds, Long userId) {
  179 + // 循环收回
  180 + for (Long couponId : giveCouponIds) {
  181 + try {
  182 + getSelf().invalidateCoupon(couponId, userId);
  183 + } catch (Exception e) {
  184 + log.error("[invalidateCouponsByAdmin][couponId({}) 收回优惠券失败]", couponId, e);
  185 + }
195 } 186 }
196 - return couponMapper.selectCountByUserIdAndTemplateIdIn(userId, templateIds); 187 + }
  188 +
  189 + /**
  190 + * 【管理员】收回优惠券
  191 + *
  192 + * @param couponId 模版编号
  193 + * @param userId 用户编号
  194 + */
  195 + @Transactional(rollbackFor = Exception.class)
  196 + public void invalidateCoupon(Long couponId, Long userId) {
  197 + // 1.1 校验优惠券
  198 + CouponDO coupon = couponMapper.selectByIdAndUserId(couponId, userId);
  199 + if (coupon == null) {
  200 + throw exception(COUPON_NOT_EXISTS);
  201 + }
  202 + // 1.2 校验模板
  203 + CouponTemplateDO couponTemplate = couponTemplateService.getCouponTemplate(coupon.getTemplateId());
  204 + if (couponTemplate == null) {
  205 + throw exception(COUPON_TEMPLATE_NOT_EXISTS);
  206 + }
  207 + // 1.3 校验优惠券是否已经使用,如若使用则先不管
  208 + if (ObjUtil.equal(coupon.getStatus(), CouponStatusEnum.USED.getStatus())) {
  209 + log.info("[invalidateCoupon][coupon({}) 已经使用,无法作废]", couponId);
  210 + return;
  211 + }
  212 +
  213 + // 2.1 减少优惠劵模板的领取数量
  214 + couponTemplateService.updateCouponTemplateTakeCount(couponTemplate.getId(), -1);
  215 + // 2.2 作废优惠劵
  216 + couponMapper.deleteById(couponId);
197 } 217 }
198 218
199 @Override 219 @Override
200 - public List<CouponDO> getMatchCouponList(Long userId, AppCouponMatchReqVO matchReqVO) {  
201 - List<CouponDO> list = couponMapper.selectListByUserIdAndStatusAndUsePriceLeAndProductScope(userId,  
202 - CouponStatusEnum.UNUSED.getStatus(),  
203 - matchReqVO.getPrice(), matchReqVO.getSpuIds(), matchReqVO.getCategoryIds());  
204 - // 兜底逻辑:如果 CouponExpireJob 未执行,status 未变成 EXPIRE ,但是 validEndTime 已经过期了,需要进行过滤  
205 - list.removeIf(coupon -> !LocalDateTimeUtils.isBetween(coupon.getValidStartTime(), coupon.getValidEndTime()));  
206 - return list; 220 + @Transactional(rollbackFor = Exception.class)
  221 + public void takeCouponByRegister(Long userId) {
  222 + List<CouponTemplateDO> templates = couponTemplateService.getCouponTemplateListByTakeType(CouponTakeTypeEnum.REGISTER);
  223 + for (CouponTemplateDO template : templates) {
  224 + takeCoupon(template.getId(), CollUtil.newHashSet(userId), CouponTakeTypeEnum.REGISTER);
  225 + }
207 } 226 }
208 227
209 @Override 228 @Override
@@ -230,27 +249,6 @@ public class CouponServiceImpl implements CouponService { @@ -230,27 +249,6 @@ public class CouponServiceImpl implements CouponService {
230 return count; 249 return count;
231 } 250 }
232 251
233 - @Override  
234 - public Map<Long, Boolean> getUserCanCanTakeMap(Long userId, List<CouponTemplateDO> templates) {  
235 - // 1. 未登录时,都显示可以领取  
236 - Map<Long, Boolean> userCanTakeMap = convertMap(templates, CouponTemplateDO::getId, templateId -> true);  
237 - if (userId == null) {  
238 - return userCanTakeMap;  
239 - }  
240 -  
241 - // 2.1 过滤领取数量无限制的  
242 - Set<Long> templateIds = convertSet(templates, CouponTemplateDO::getId, template -> template.getTakeLimitCount() != -1);  
243 - // 2.2 检查用户领取的数量是否超过限制  
244 - if (CollUtil.isNotEmpty(templateIds)) {  
245 - Map<Long, Integer> couponTakeCountMap = this.getTakeCountMapByTemplateIds(templateIds, userId);  
246 - for (CouponTemplateDO template : templates) {  
247 - Integer takeCount = couponTakeCountMap.get(template.getId());  
248 - userCanTakeMap.put(template.getId(), takeCount == null || takeCount < template.getTakeLimitCount());  
249 - }  
250 - }  
251 - return userCanTakeMap;  
252 - }  
253 -  
254 /** 252 /**
255 * 过期单个优惠劵 253 * 过期单个优惠劵
256 * 254 *
@@ -322,11 +320,84 @@ public class CouponServiceImpl implements CouponService { @@ -322,11 +320,84 @@ public class CouponServiceImpl implements CouponService {
322 userIds.removeIf(userId -> MapUtil.getInt(userTakeCountMap, userId, 0) >= couponTemplate.getTakeLimitCount()); 320 userIds.removeIf(userId -> MapUtil.getInt(userTakeCountMap, userId, 0) >= couponTemplate.getTakeLimitCount());
323 } 321 }
324 322
  323 + //======================= 查询相关 =======================
  324 +
  325 + @Override
  326 + public Long getUnusedCouponCount(Long userId) {
  327 + return couponMapper.selectCountByUserIdAndStatus(userId, CouponStatusEnum.UNUSED.getStatus());
  328 + }
  329 +
  330 + @Override
  331 + public PageResult<CouponDO> getCouponPage(CouponPageReqVO pageReqVO) {
  332 + // 获得用户编号
  333 + if (StrUtil.isNotEmpty(pageReqVO.getNickname())) {
  334 + List<MemberUserRespDTO> users = memberUserApi.getUserListByNickname(pageReqVO.getNickname());
  335 + if (CollUtil.isEmpty(users)) {
  336 + return PageResult.empty();
  337 + }
  338 + pageReqVO.setUserIds(convertSet(users, MemberUserRespDTO::getId));
  339 + }
  340 + // 分页查询
  341 + return couponMapper.selectPage(pageReqVO);
  342 + }
  343 +
  344 + @Override
  345 + public List<CouponDO> getCouponList(Long userId, Integer status) {
  346 + return couponMapper.selectListByUserIdAndStatus(userId, status);
  347 + }
  348 +
  349 + @Override
  350 + public Map<Long, Integer> getTakeCountMapByTemplateIds(Collection<Long> templateIds, Long userId) {
  351 + if (CollUtil.isEmpty(templateIds)) {
  352 + return Collections.emptyMap();
  353 + }
  354 + return couponMapper.selectCountByUserIdAndTemplateIdIn(userId, templateIds);
  355 + }
  356 +
  357 + @Override
  358 + public List<CouponDO> getMatchCouponList(Long userId, AppCouponMatchReqVO matchReqVO) {
  359 + List<CouponDO> list = couponMapper.selectListByUserIdAndStatusAndUsePriceLeAndProductScope(userId,
  360 + CouponStatusEnum.UNUSED.getStatus(),
  361 + matchReqVO.getPrice(), matchReqVO.getSpuIds(), matchReqVO.getCategoryIds());
  362 + // 兜底逻辑:如果 CouponExpireJob 未执行,status 未变成 EXPIRE ,但是 validEndTime 已经过期了,需要进行过滤
  363 + list.removeIf(coupon -> !LocalDateTimeUtils.isBetween(coupon.getValidStartTime(), coupon.getValidEndTime()));
  364 + return list;
  365 + }
  366 +
  367 + @Override
  368 + public Map<Long, Boolean> getUserCanCanTakeMap(Long userId, List<CouponTemplateDO> templates) {
  369 + // 1. 未登录时,都显示可以领取
  370 + Map<Long, Boolean> userCanTakeMap = convertMap(templates, CouponTemplateDO::getId, templateId -> true);
  371 + if (userId == null) {
  372 + return userCanTakeMap;
  373 + }
  374 +
  375 + // 2.1 过滤领取数量无限制的
  376 + Set<Long> templateIds = convertSet(templates, CouponTemplateDO::getId, template -> template.getTakeLimitCount() != -1);
  377 + // 2.2 检查用户领取的数量是否超过限制
  378 + if (CollUtil.isNotEmpty(templateIds)) {
  379 + Map<Long, Integer> couponTakeCountMap = this.getTakeCountMapByTemplateIds(templateIds, userId);
  380 + for (CouponTemplateDO template : templates) {
  381 + Integer takeCount = couponTakeCountMap.get(template.getId());
  382 + userCanTakeMap.put(template.getId(), takeCount == null || takeCount < template.getTakeLimitCount());
  383 + }
  384 + }
  385 + return userCanTakeMap;
  386 + }
  387 +
325 @Override 388 @Override
326 public CouponDO getCoupon(Long userId, Long id) { 389 public CouponDO getCoupon(Long userId, Long id) {
327 return couponMapper.selectByIdAndUserId(id, userId); 390 return couponMapper.selectByIdAndUserId(id, userId);
328 } 391 }
329 392
  393 + private CouponDO validateCouponExists(Long id) {
  394 + CouponDO coupon = couponMapper.selectById(id);
  395 + if (coupon == null) {
  396 + throw exception(COUPON_NOT_EXISTS);
  397 + }
  398 + return coupon;
  399 + }
  400 +
330 /** 401 /**
331 * 获得自身的代理对象,解决 AOP 生效问题 402 * 获得自身的代理对象,解决 AOP 生效问题
332 * 403 *
@@ -335,4 +406,5 @@ public class CouponServiceImpl implements CouponService { @@ -335,4 +406,5 @@ public class CouponServiceImpl implements CouponService {
335 private CouponServiceImpl getSelf() { 406 private CouponServiceImpl getSelf() {
336 return SpringUtil.getBean(getClass()); 407 return SpringUtil.getBean(getClass());
337 } 408 }
  409 +
338 } 410 }
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 { @@ -104,7 +104,10 @@ public class DiscountActivityServiceImpl implements DiscountActivityService {
104 } 104 }
105 // 计算新增的记录 105 // 计算新增的记录
106 List<DiscountProductDO> newDiscountProducts = convertList(updateReqVO.getProducts(), 106 List<DiscountProductDO> newDiscountProducts = convertList(updateReqVO.getProducts(),
107 - product -> DiscountActivityConvert.INSTANCE.convert(product).setActivityId(updateReqVO.getId())); 107 + product -> DiscountActivityConvert.INSTANCE.convert(product)
  108 + .setActivityId(updateReqVO.getId())
  109 + .setActivityStartTime(updateReqVO.getStartTime())
  110 + .setActivityEndTime(updateReqVO.getEndTime()));
108 newDiscountProducts.removeIf(product -> dbDiscountProducts.stream().anyMatch( 111 newDiscountProducts.removeIf(product -> dbDiscountProducts.stream().anyMatch(
109 dbProduct -> DiscountActivityConvert.INSTANCE.isEquals(dbProduct, product))); // 如果匹配到,说明是更新的 112 dbProduct -> DiscountActivityConvert.INSTANCE.isEquals(dbProduct, product))); // 如果匹配到,说明是更新的
110 if (CollectionUtil.isNotEmpty(newDiscountProducts)) { 113 if (CollectionUtil.isNotEmpty(newDiscountProducts)) {
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 { @@ -75,11 +75,10 @@ public interface RewardActivityService {
75 /** 75 /**
76 * 获取指定 spu 编号最近参加的活动,每个 spuId 只返回一条记录 76 * 获取指定 spu 编号最近参加的活动,每个 spuId 只返回一条记录
77 * 77 *
78 - * @param spuIds spu 编号  
79 * @param status 状态 78 * @param status 状态
80 * @param dateTime 当前日期时间 79 * @param dateTime 当前日期时间
81 * @return 满减送活动列表 80 * @return 满减送活动列表
82 */ 81 */
83 - List<RewardActivityDO> getRewardActivityBySpuIdsAndStatusAndDateTimeLt(Collection<Long> spuIds, Integer status, LocalDateTime dateTime); 82 + List<RewardActivityDO> getRewardActivityListByStatusAndDateTimeLt(Integer status, LocalDateTime dateTime);
84 83
85 } 84 }
yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/service/reward/RewardActivityServiceImpl.java
1 package cn.iocoder.yudao.module.promotion.service.reward; 1 package cn.iocoder.yudao.module.promotion.service.reward;
2 2
3 -import cn.hutool.core.collection.CollUtil; 3 +import cn.iocoder.yudao.framework.common.enums.CommonStatusEnum;
4 import cn.iocoder.yudao.framework.common.pojo.PageResult; 4 import cn.iocoder.yudao.framework.common.pojo.PageResult;
  5 +import cn.iocoder.yudao.framework.common.util.object.BeanUtils;
  6 +import cn.iocoder.yudao.module.product.api.category.ProductCategoryApi;
  7 +import cn.iocoder.yudao.module.product.api.spu.ProductSpuApi;
5 import cn.iocoder.yudao.module.promotion.api.reward.dto.RewardActivityMatchRespDTO; 8 import cn.iocoder.yudao.module.promotion.api.reward.dto.RewardActivityMatchRespDTO;
  9 +import cn.iocoder.yudao.module.promotion.controller.admin.reward.vo.RewardActivityBaseVO;
6 import cn.iocoder.yudao.module.promotion.controller.admin.reward.vo.RewardActivityCreateReqVO; 10 import cn.iocoder.yudao.module.promotion.controller.admin.reward.vo.RewardActivityCreateReqVO;
7 import cn.iocoder.yudao.module.promotion.controller.admin.reward.vo.RewardActivityPageReqVO; 11 import cn.iocoder.yudao.module.promotion.controller.admin.reward.vo.RewardActivityPageReqVO;
8 import cn.iocoder.yudao.module.promotion.controller.admin.reward.vo.RewardActivityUpdateReqVO; 12 import cn.iocoder.yudao.module.promotion.controller.admin.reward.vo.RewardActivityUpdateReqVO;
9 -import cn.iocoder.yudao.module.promotion.convert.reward.RewardActivityConvert;  
10 import cn.iocoder.yudao.module.promotion.dal.dataobject.reward.RewardActivityDO; 13 import cn.iocoder.yudao.module.promotion.dal.dataobject.reward.RewardActivityDO;
11 import cn.iocoder.yudao.module.promotion.dal.mysql.reward.RewardActivityMapper; 14 import cn.iocoder.yudao.module.promotion.dal.mysql.reward.RewardActivityMapper;
12 -import cn.iocoder.yudao.module.promotion.enums.common.PromotionActivityStatusEnum; 15 +import cn.iocoder.yudao.module.promotion.enums.common.PromotionProductScopeEnum;
13 import cn.iocoder.yudao.module.promotion.util.PromotionUtils; 16 import cn.iocoder.yudao.module.promotion.util.PromotionUtils;
14 import jakarta.annotation.Resource; 17 import jakarta.annotation.Resource;
15 import org.springframework.stereotype.Service; 18 import org.springframework.stereotype.Service;
@@ -17,13 +20,13 @@ import org.springframework.validation.annotation.Validated; @@ -17,13 +20,13 @@ import org.springframework.validation.annotation.Validated;
17 20
18 import java.time.LocalDateTime; 21 import java.time.LocalDateTime;
19 import java.util.Collection; 22 import java.util.Collection;
20 -import java.util.Collections;  
21 import java.util.List; 23 import java.util.List;
  24 +import java.util.Objects;
22 25
  26 +import static cn.hutool.core.collection.CollUtil.intersectionDistinct;
23 import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception; 27 import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception;
24 -import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertSet; 28 +import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.anyMatch;
25 import static cn.iocoder.yudao.module.promotion.enums.ErrorCodeConstants.*; 29 import static cn.iocoder.yudao.module.promotion.enums.ErrorCodeConstants.*;
26 -import static java.util.Arrays.asList;  
27 30
28 /** 31 /**
29 * 满减送活动 Service 实现类 32 * 满减送活动 Service 实现类
@@ -37,13 +40,20 @@ public class RewardActivityServiceImpl implements RewardActivityService { @@ -37,13 +40,20 @@ public class RewardActivityServiceImpl implements RewardActivityService {
37 @Resource 40 @Resource
38 private RewardActivityMapper rewardActivityMapper; 41 private RewardActivityMapper rewardActivityMapper;
39 42
  43 + @Resource
  44 + private ProductCategoryApi productCategoryApi;
  45 + @Resource
  46 + private ProductSpuApi productSpuApi;
  47 +
40 @Override 48 @Override
41 public Long createRewardActivity(RewardActivityCreateReqVO createReqVO) { 49 public Long createRewardActivity(RewardActivityCreateReqVO createReqVO) {
42 - // 校验商品是否冲突  
43 - validateRewardActivitySpuConflicts(null, createReqVO.getProductSpuIds()); 50 + // 1.1 校验商品范围
  51 + validateProductScope(createReqVO.getProductScope(), createReqVO.getProductScopeValues());
  52 + // 1.2 校验商品是否冲突
  53 + validateRewardActivitySpuConflicts(null, createReqVO);
44 54
45 - // 插入  
46 - RewardActivityDO rewardActivity = RewardActivityConvert.INSTANCE.convert(createReqVO) 55 + // 2. 插入
  56 + RewardActivityDO rewardActivity = BeanUtils.toBean(createReqVO, RewardActivityDO.class)
47 .setStatus(PromotionUtils.calculateActivityStatus(createReqVO.getEndTime())); 57 .setStatus(PromotionUtils.calculateActivityStatus(createReqVO.getEndTime()));
48 rewardActivityMapper.insert(rewardActivity); 58 rewardActivityMapper.insert(rewardActivity);
49 // 返回 59 // 返回
@@ -52,16 +62,18 @@ public class RewardActivityServiceImpl implements RewardActivityService { @@ -52,16 +62,18 @@ public class RewardActivityServiceImpl implements RewardActivityService {
52 62
53 @Override 63 @Override
54 public void updateRewardActivity(RewardActivityUpdateReqVO updateReqVO) { 64 public void updateRewardActivity(RewardActivityUpdateReqVO updateReqVO) {
55 - // 校验存在 65 + // 1.1 校验存在
56 RewardActivityDO dbRewardActivity = validateRewardActivityExists(updateReqVO.getId()); 66 RewardActivityDO dbRewardActivity = validateRewardActivityExists(updateReqVO.getId());
57 - if (dbRewardActivity.getStatus().equals(PromotionActivityStatusEnum.CLOSE.getStatus())) { // 已关闭的活动,不能修改噢 67 + if (dbRewardActivity.getStatus().equals(CommonStatusEnum.DISABLE.getStatus())) { // 已关闭的活动,不能修改噢
58 throw exception(REWARD_ACTIVITY_UPDATE_FAIL_STATUS_CLOSED); 68 throw exception(REWARD_ACTIVITY_UPDATE_FAIL_STATUS_CLOSED);
59 } 69 }
60 - // 校验商品是否冲突  
61 - validateRewardActivitySpuConflicts(updateReqVO.getId(), updateReqVO.getProductSpuIds()); 70 + // 1.2 校验商品范围
  71 + validateProductScope(updateReqVO.getProductScope(), updateReqVO.getProductScopeValues());
  72 + // 1.3 校验商品是否冲突
  73 + validateRewardActivitySpuConflicts(updateReqVO.getId(), updateReqVO);
62 74
63 - // 更新  
64 - RewardActivityDO updateObj = RewardActivityConvert.INSTANCE.convert(updateReqVO) 75 + // 2. 更新
  76 + RewardActivityDO updateObj = BeanUtils.toBean(updateReqVO, RewardActivityDO.class)
65 .setStatus(PromotionUtils.calculateActivityStatus(updateReqVO.getEndTime())); 77 .setStatus(PromotionUtils.calculateActivityStatus(updateReqVO.getEndTime()));
66 rewardActivityMapper.updateById(updateObj); 78 rewardActivityMapper.updateById(updateObj);
67 } 79 }
@@ -70,15 +82,12 @@ public class RewardActivityServiceImpl implements RewardActivityService { @@ -70,15 +82,12 @@ public class RewardActivityServiceImpl implements RewardActivityService {
70 public void closeRewardActivity(Long id) { 82 public void closeRewardActivity(Long id) {
71 // 校验存在 83 // 校验存在
72 RewardActivityDO dbRewardActivity = validateRewardActivityExists(id); 84 RewardActivityDO dbRewardActivity = validateRewardActivityExists(id);
73 - if (dbRewardActivity.getStatus().equals(PromotionActivityStatusEnum.CLOSE.getStatus())) { // 已关闭的活动,不能关闭噢 85 + if (dbRewardActivity.getStatus().equals(CommonStatusEnum.DISABLE.getStatus())) { // 已关闭的活动,不能关闭噢
74 throw exception(REWARD_ACTIVITY_CLOSE_FAIL_STATUS_CLOSED); 86 throw exception(REWARD_ACTIVITY_CLOSE_FAIL_STATUS_CLOSED);
75 } 87 }
76 - if (dbRewardActivity.getStatus().equals(PromotionActivityStatusEnum.END.getStatus())) { // 已关闭的活动,不能关闭噢  
77 - throw exception(REWARD_ACTIVITY_CLOSE_FAIL_STATUS_END);  
78 - }  
79 88
80 // 更新 89 // 更新
81 - RewardActivityDO updateObj = new RewardActivityDO().setId(id).setStatus(PromotionActivityStatusEnum.CLOSE.getStatus()); 90 + RewardActivityDO updateObj = new RewardActivityDO().setId(id).setStatus(CommonStatusEnum.DISABLE.getStatus());
82 rewardActivityMapper.updateById(updateObj); 91 rewardActivityMapper.updateById(updateObj);
83 } 92 }
84 93
@@ -86,7 +95,7 @@ public class RewardActivityServiceImpl implements RewardActivityService { @@ -86,7 +95,7 @@ public class RewardActivityServiceImpl implements RewardActivityService {
86 public void deleteRewardActivity(Long id) { 95 public void deleteRewardActivity(Long id) {
87 // 校验存在 96 // 校验存在
88 RewardActivityDO dbRewardActivity = validateRewardActivityExists(id); 97 RewardActivityDO dbRewardActivity = validateRewardActivityExists(id);
89 - if (!dbRewardActivity.getStatus().equals(PromotionActivityStatusEnum.CLOSE.getStatus())) { // 未关闭的活动,不能删除噢 98 + if (dbRewardActivity.getStatus().equals(CommonStatusEnum.ENABLE.getStatus())) { // 未关闭的活动,不能删除噢
90 throw exception(REWARD_ACTIVITY_DELETE_FAIL_STATUS_NOT_CLOSED); 99 throw exception(REWARD_ACTIVITY_DELETE_FAIL_STATUS_NOT_CLOSED);
91 } 100 }
92 101
@@ -102,41 +111,39 @@ public class RewardActivityServiceImpl implements RewardActivityService { @@ -102,41 +111,39 @@ public class RewardActivityServiceImpl implements RewardActivityService {
102 return activity; 111 return activity;
103 } 112 }
104 113
105 - // TODO @芋艿:逻辑有问题,需要优化;要分成全场、和指定来校验;  
106 -  
107 /** 114 /**
108 * 校验商品参加的活动是否冲突 115 * 校验商品参加的活动是否冲突
109 * 116 *
110 - * @param id 活动编号  
111 - * @param spuIds 商品 SPU 编号数组 117 + * @param id 活动编号
  118 + * @param rewardActivity 请求
112 */ 119 */
113 - private void validateRewardActivitySpuConflicts(Long id, Collection<Long> spuIds) {  
114 - if (CollUtil.isEmpty(spuIds)) {  
115 - return;  
116 - }  
117 - // 查询商品参加的活动  
118 - List<RewardActivityDO> rewardActivityList = getRewardActivityListBySpuIds(spuIds,  
119 - asList(PromotionActivityStatusEnum.WAIT.getStatus(), PromotionActivityStatusEnum.RUN.getStatus())); 120 + private void validateRewardActivitySpuConflicts(Long id, RewardActivityBaseVO rewardActivity) {
  121 + List<RewardActivityDO> list = rewardActivityMapper.selectList(RewardActivityDO::getProductScope,
  122 + rewardActivity.getProductScope(), RewardActivityDO::getStatus, CommonStatusEnum.ENABLE.getStatus());
120 if (id != null) { // 排除自己这个活动 123 if (id != null) { // 排除自己这个活动
121 - rewardActivityList.removeIf(activity -> id.equals(activity.getId())); 124 + list.removeIf(activity -> id.equals(activity.getId()));
122 } 125 }
123 - // 如果非空,则说明冲突  
124 - if (CollUtil.isNotEmpty(rewardActivityList)) {  
125 - throw exception(REWARD_ACTIVITY_SPU_CONFLICTS); 126 +
  127 + // 情况一:全部商品参加
  128 + if (PromotionProductScopeEnum.isAll(rewardActivity.getProductScope()) && !list.isEmpty()) {
  129 + throw exception(REWARD_ACTIVITY_SCOPE_ALL_EXISTS);
  130 + }
  131 + if (PromotionProductScopeEnum.isSpu(rewardActivity.getProductScope()) || // 情况二:指定商品参加
  132 + PromotionProductScopeEnum.isCategory(rewardActivity.getProductScope())) { // 情况三:指定商品类型参加
  133 + if (anyMatch(list, item -> !intersectionDistinct(item.getProductScopeValues(),
  134 + rewardActivity.getProductScopeValues()).isEmpty())) {
  135 + throw exception(PromotionProductScopeEnum.isSpu(rewardActivity.getProductScope()) ?
  136 + REWARD_ACTIVITY_SPU_CONFLICTS : REWARD_ACTIVITY_SCOPE_CATEGORY_EXISTS);
  137 + }
126 } 138 }
127 } 139 }
128 140
129 - /**  
130 - * 获得商品参加的满减送活动的数组  
131 - *  
132 - * @param spuIds 商品 SPU 编号数组  
133 - * @param statuses 活动状态数组  
134 - * @return 商品参加的满减送活动的数组  
135 - */  
136 - private List<RewardActivityDO> getRewardActivityListBySpuIds(Collection<Long> spuIds,  
137 - Collection<Integer> statuses) {  
138 - List<RewardActivityDO> list = rewardActivityMapper.selectListByStatus(statuses);  
139 - return CollUtil.filter(list, activity -> CollUtil.containsAny(activity.getProductSpuIds(), spuIds)); 141 + private void validateProductScope(Integer productScope, List<Long> productScopeValues) {
  142 + if (Objects.equals(PromotionProductScopeEnum.SPU.getScope(), productScope)) {
  143 + productSpuApi.validateSpuList(productScopeValues);
  144 + } else if (Objects.equals(PromotionProductScopeEnum.CATEGORY.getScope(), productScope)) {
  145 + productCategoryApi.validateCategoryList(productScopeValues);
  146 + }
140 } 147 }
141 148
142 @Override 149 @Override
@@ -151,32 +158,13 @@ public class RewardActivityServiceImpl implements RewardActivityService { @@ -151,32 +158,13 @@ public class RewardActivityServiceImpl implements RewardActivityService {
151 158
152 @Override 159 @Override
153 public List<RewardActivityMatchRespDTO> getMatchRewardActivityList(Collection<Long> spuIds) { 160 public List<RewardActivityMatchRespDTO> getMatchRewardActivityList(Collection<Long> spuIds) {
154 - // TODO 芋艿:待实现;先指定,然后再全局的;  
155 -// // 如果有全局活动,则直接选择它  
156 -// List<RewardActivityDO> allActivities = rewardActivityMapper.selectListByProductScopeAndStatus(  
157 -// PromotionProductScopeEnum.ALL.getScope(), PromotionActivityStatusEnum.RUN.getStatus());  
158 -// if (CollUtil.isNotEmpty(allActivities)) {  
159 -// return MapUtil.builder(allActivities.get(0), spuIds).build();  
160 -// }  
161 -//  
162 -// // 查询某个活动参加的活动  
163 -// List<RewardActivityDO> productActivityList = getRewardActivityListBySpuIds(spuIds,  
164 -// singleton(PromotionActivityStatusEnum.RUN.getStatus()));  
165 -// return convertMap(productActivityList, activity -> activity,  
166 -// rewardActivityDO -> intersectionDistinct(rewardActivityDO.getProductSpuIds(), spuIds)); // 求交集返回  
167 - return null; 161 + List<RewardActivityDO> list = rewardActivityMapper.selectListBySpuIdsAndStatus(spuIds, CommonStatusEnum.ENABLE.getStatus());
  162 + return BeanUtils.toBean(list, RewardActivityMatchRespDTO.class);
168 } 163 }
169 164
170 @Override 165 @Override
171 - public List<RewardActivityDO> getRewardActivityBySpuIdsAndStatusAndDateTimeLt(Collection<Long> spuIds, Integer status, LocalDateTime dateTime) {  
172 - // 1. 查询出指定 spuId 的 spu 参加的活动  
173 - List<RewardActivityDO> rewardActivityList = rewardActivityMapper.selectListBySpuIdsAndStatus(spuIds, status);  
174 - if (CollUtil.isEmpty(rewardActivityList)) {  
175 - return Collections.emptyList();  
176 - }  
177 -  
178 - // 2. 查询活动详情  
179 - return rewardActivityMapper.selectListByIdsAndDateTimeLt(convertSet(rewardActivityList, RewardActivityDO::getId), dateTime); 166 + public List<RewardActivityDO> getRewardActivityListByStatusAndDateTimeLt(Integer status, LocalDateTime dateTime) {
  167 + return rewardActivityMapper.selectListByStatusAndDateTimeLt(status, dateTime);
180 } 168 }
181 169
182 } 170 }
yudao-module-mall/yudao-module-promotion-biz/src/test/java/cn/iocoder/yudao/module/promotion/service/reward/RewardActivityServiceImplTest.java
1 package cn.iocoder.yudao.module.promotion.service.reward; 1 package cn.iocoder.yudao.module.promotion.service.reward;
2 2
  3 +import cn.iocoder.yudao.framework.common.enums.CommonStatusEnum;
3 import cn.iocoder.yudao.framework.common.pojo.PageResult; 4 import cn.iocoder.yudao.framework.common.pojo.PageResult;
4 import cn.iocoder.yudao.framework.test.core.ut.BaseDbUnitTest; 5 import cn.iocoder.yudao.framework.test.core.ut.BaseDbUnitTest;
  6 +import cn.iocoder.yudao.module.promotion.api.reward.dto.RewardActivityMatchRespDTO;
5 import cn.iocoder.yudao.module.promotion.controller.admin.reward.vo.RewardActivityCreateReqVO; 7 import cn.iocoder.yudao.module.promotion.controller.admin.reward.vo.RewardActivityCreateReqVO;
6 import cn.iocoder.yudao.module.promotion.controller.admin.reward.vo.RewardActivityPageReqVO; 8 import cn.iocoder.yudao.module.promotion.controller.admin.reward.vo.RewardActivityPageReqVO;
7 import cn.iocoder.yudao.module.promotion.controller.admin.reward.vo.RewardActivityUpdateReqVO; 9 import cn.iocoder.yudao.module.promotion.controller.admin.reward.vo.RewardActivityUpdateReqVO;
8 import cn.iocoder.yudao.module.promotion.dal.dataobject.reward.RewardActivityDO; 10 import cn.iocoder.yudao.module.promotion.dal.dataobject.reward.RewardActivityDO;
9 import cn.iocoder.yudao.module.promotion.dal.mysql.reward.RewardActivityMapper; 11 import cn.iocoder.yudao.module.promotion.dal.mysql.reward.RewardActivityMapper;
10 -import cn.iocoder.yudao.module.promotion.enums.common.PromotionActivityStatusEnum;  
11 import cn.iocoder.yudao.module.promotion.enums.common.PromotionConditionTypeEnum; 12 import cn.iocoder.yudao.module.promotion.enums.common.PromotionConditionTypeEnum;
12 import cn.iocoder.yudao.module.promotion.enums.common.PromotionProductScopeEnum; 13 import cn.iocoder.yudao.module.promotion.enums.common.PromotionProductScopeEnum;
  14 +import jakarta.annotation.Resource;
13 import org.junit.jupiter.api.Disabled; 15 import org.junit.jupiter.api.Disabled;
14 import org.junit.jupiter.api.Test; 16 import org.junit.jupiter.api.Test;
15 import org.springframework.context.annotation.Import; 17 import org.springframework.context.annotation.Import;
16 18
17 -import jakarta.annotation.Resource;  
18 import java.time.Duration; 19 import java.time.Duration;
  20 +import java.util.List;
19 import java.util.Set; 21 import java.util.Set;
20 22
21 import static cn.hutool.core.util.RandomUtil.randomEle; 23 import static cn.hutool.core.util.RandomUtil.randomEle;
@@ -27,15 +29,15 @@ import static cn.iocoder.yudao.framework.test.core.util.AssertUtils.assertServic @@ -27,15 +29,15 @@ import static cn.iocoder.yudao.framework.test.core.util.AssertUtils.assertServic
27 import static cn.iocoder.yudao.framework.test.core.util.RandomUtils.randomLongId; 29 import static cn.iocoder.yudao.framework.test.core.util.RandomUtils.randomLongId;
28 import static cn.iocoder.yudao.framework.test.core.util.RandomUtils.randomPojo; 30 import static cn.iocoder.yudao.framework.test.core.util.RandomUtils.randomPojo;
29 import static cn.iocoder.yudao.module.promotion.enums.ErrorCodeConstants.REWARD_ACTIVITY_NOT_EXISTS; 31 import static cn.iocoder.yudao.module.promotion.enums.ErrorCodeConstants.REWARD_ACTIVITY_NOT_EXISTS;
30 -import static java.util.Arrays.asList; 32 +import static com.google.common.primitives.Longs.asList;
31 import static java.util.Collections.singletonList; 33 import static java.util.Collections.singletonList;
32 import static org.junit.jupiter.api.Assertions.*; 34 import static org.junit.jupiter.api.Assertions.*;
33 35
34 /** 36 /**
35 -* {@link RewardActivityServiceImpl} 的单元测试类  
36 -*  
37 -* @author 芋道源码  
38 -*/ 37 + * {@link RewardActivityServiceImpl} 的单元测试类
  38 + *
  39 + * @author 芋道源码
  40 + */
39 @Disabled // TODO 芋艿:后续 fix 补充的单测 41 @Disabled // TODO 芋艿:后续 fix 补充的单测
40 @Import(RewardActivityServiceImpl.class) 42 @Import(RewardActivityServiceImpl.class)
41 public class RewardActivityServiceImplTest extends BaseDbUnitTest { 43 public class RewardActivityServiceImplTest extends BaseDbUnitTest {
@@ -63,7 +65,7 @@ public class RewardActivityServiceImplTest extends BaseDbUnitTest { @@ -63,7 +65,7 @@ public class RewardActivityServiceImplTest extends BaseDbUnitTest {
63 // 校验记录的属性是否正确 65 // 校验记录的属性是否正确
64 RewardActivityDO rewardActivity = rewardActivityMapper.selectById(rewardActivityId); 66 RewardActivityDO rewardActivity = rewardActivityMapper.selectById(rewardActivityId);
65 assertPojoEquals(reqVO, rewardActivity, "rules"); 67 assertPojoEquals(reqVO, rewardActivity, "rules");
66 - assertEquals(rewardActivity.getStatus(), PromotionActivityStatusEnum.WAIT.getStatus()); 68 + assertEquals(rewardActivity.getStatus(), CommonStatusEnum.DISABLE.getStatus());
67 for (int i = 0; i < reqVO.getRules().size(); i++) { 69 for (int i = 0; i < reqVO.getRules().size(); i++) {
68 assertPojoEquals(reqVO.getRules().get(i), rewardActivity.getRules().get(i)); 70 assertPojoEquals(reqVO.getRules().get(i), rewardActivity.getRules().get(i));
69 } 71 }
@@ -72,7 +74,7 @@ public class RewardActivityServiceImplTest extends BaseDbUnitTest { @@ -72,7 +74,7 @@ public class RewardActivityServiceImplTest extends BaseDbUnitTest {
72 @Test 74 @Test
73 public void testUpdateRewardActivity_success() { 75 public void testUpdateRewardActivity_success() {
74 // mock 数据 76 // mock 数据
75 - RewardActivityDO dbRewardActivity = randomPojo(RewardActivityDO.class, o -> o.setStatus(PromotionActivityStatusEnum.WAIT.getStatus())); 77 + RewardActivityDO dbRewardActivity = randomPojo(RewardActivityDO.class, o -> o.setStatus(CommonStatusEnum.DISABLE.getStatus()));
76 rewardActivityMapper.insert(dbRewardActivity);// @Sql: 先插入出一条存在的数据 78 rewardActivityMapper.insert(dbRewardActivity);// @Sql: 先插入出一条存在的数据
77 // 准备参数 79 // 准备参数
78 RewardActivityUpdateReqVO reqVO = randomPojo(RewardActivityUpdateReqVO.class, o -> { 80 RewardActivityUpdateReqVO reqVO = randomPojo(RewardActivityUpdateReqVO.class, o -> {
@@ -88,7 +90,7 @@ public class RewardActivityServiceImplTest extends BaseDbUnitTest { @@ -88,7 +90,7 @@ public class RewardActivityServiceImplTest extends BaseDbUnitTest {
88 // 校验是否更新正确 90 // 校验是否更新正确
89 RewardActivityDO rewardActivity = rewardActivityMapper.selectById(reqVO.getId()); // 获取最新的 91 RewardActivityDO rewardActivity = rewardActivityMapper.selectById(reqVO.getId()); // 获取最新的
90 assertPojoEquals(reqVO, rewardActivity, "rules"); 92 assertPojoEquals(reqVO, rewardActivity, "rules");
91 - assertEquals(rewardActivity.getStatus(), PromotionActivityStatusEnum.WAIT.getStatus()); 93 + assertEquals(rewardActivity.getStatus(), CommonStatusEnum.DISABLE.getStatus());
92 for (int i = 0; i < reqVO.getRules().size(); i++) { 94 for (int i = 0; i < reqVO.getRules().size(); i++) {
93 assertPojoEquals(reqVO.getRules().get(i), rewardActivity.getRules().get(i)); 95 assertPojoEquals(reqVO.getRules().get(i), rewardActivity.getRules().get(i));
94 } 96 }
@@ -97,7 +99,7 @@ public class RewardActivityServiceImplTest extends BaseDbUnitTest { @@ -97,7 +99,7 @@ public class RewardActivityServiceImplTest extends BaseDbUnitTest {
97 @Test 99 @Test
98 public void testCloseRewardActivity() { 100 public void testCloseRewardActivity() {
99 // mock 数据 101 // mock 数据
100 - RewardActivityDO dbRewardActivity = randomPojo(RewardActivityDO.class, o -> o.setStatus(PromotionActivityStatusEnum.WAIT.getStatus())); 102 + RewardActivityDO dbRewardActivity = randomPojo(RewardActivityDO.class, o -> o.setStatus(CommonStatusEnum.DISABLE.getStatus()));
101 rewardActivityMapper.insert(dbRewardActivity);// @Sql: 先插入出一条存在的数据 103 rewardActivityMapper.insert(dbRewardActivity);// @Sql: 先插入出一条存在的数据
102 // 准备参数 104 // 准备参数
103 Long id = dbRewardActivity.getId(); 105 Long id = dbRewardActivity.getId();
@@ -106,7 +108,7 @@ public class RewardActivityServiceImplTest extends BaseDbUnitTest { @@ -106,7 +108,7 @@ public class RewardActivityServiceImplTest extends BaseDbUnitTest {
106 rewardActivityService.closeRewardActivity(id); 108 rewardActivityService.closeRewardActivity(id);
107 // 校验状态 109 // 校验状态
108 RewardActivityDO rewardActivity = rewardActivityMapper.selectById(id); 110 RewardActivityDO rewardActivity = rewardActivityMapper.selectById(id);
109 - assertEquals(rewardActivity.getStatus(), PromotionActivityStatusEnum.CLOSE.getStatus()); 111 + assertEquals(rewardActivity.getStatus(), CommonStatusEnum.DISABLE.getStatus());
110 } 112 }
111 113
112 @Test 114 @Test
@@ -121,15 +123,15 @@ public class RewardActivityServiceImplTest extends BaseDbUnitTest { @@ -121,15 +123,15 @@ public class RewardActivityServiceImplTest extends BaseDbUnitTest {
121 @Test 123 @Test
122 public void testDeleteRewardActivity_success() { 124 public void testDeleteRewardActivity_success() {
123 // mock 数据 125 // mock 数据
124 - RewardActivityDO dbRewardActivity = randomPojo(RewardActivityDO.class, o -> o.setStatus(PromotionActivityStatusEnum.CLOSE.getStatus())); 126 + RewardActivityDO dbRewardActivity = randomPojo(RewardActivityDO.class, o -> o.setStatus(CommonStatusEnum.DISABLE.getStatus()));
125 rewardActivityMapper.insert(dbRewardActivity);// @Sql: 先插入出一条存在的数据 127 rewardActivityMapper.insert(dbRewardActivity);// @Sql: 先插入出一条存在的数据
126 // 准备参数 128 // 准备参数
127 Long id = dbRewardActivity.getId(); 129 Long id = dbRewardActivity.getId();
128 130
129 // 调用 131 // 调用
130 rewardActivityService.deleteRewardActivity(id); 132 rewardActivityService.deleteRewardActivity(id);
131 - // 校验数据不存在了  
132 - assertNull(rewardActivityMapper.selectById(id)); 133 + // 校验数据不存在了
  134 + assertNull(rewardActivityMapper.selectById(id));
133 } 135 }
134 136
135 @Test 137 @Test
@@ -143,77 +145,82 @@ public class RewardActivityServiceImplTest extends BaseDbUnitTest { @@ -143,77 +145,82 @@ public class RewardActivityServiceImplTest extends BaseDbUnitTest {
143 145
144 @Test 146 @Test
145 public void testGetRewardActivityPage() { 147 public void testGetRewardActivityPage() {
146 - // mock 数据  
147 - RewardActivityDO dbRewardActivity = randomPojo(RewardActivityDO.class, o -> { // 等会查询到  
148 - o.setName("芋艿");  
149 - o.setStatus(PromotionActivityStatusEnum.CLOSE.getStatus());  
150 - });  
151 - rewardActivityMapper.insert(dbRewardActivity);  
152 - // 测试 name 不匹配  
153 - rewardActivityMapper.insert(cloneIgnoreId(dbRewardActivity, o -> o.setName("土豆")));  
154 - // 测试 status 不匹配  
155 - rewardActivityMapper.insert(cloneIgnoreId(dbRewardActivity, o -> o.setStatus(PromotionActivityStatusEnum.RUN.getStatus())));  
156 - // 准备参数  
157 - RewardActivityPageReqVO reqVO = new RewardActivityPageReqVO();  
158 - reqVO.setName("芋艿");  
159 - reqVO.setStatus(PromotionActivityStatusEnum.CLOSE.getStatus());  
160 -  
161 - // 调用  
162 - PageResult<RewardActivityDO> pageResult = rewardActivityService.getRewardActivityPage(reqVO);  
163 - // 断言  
164 - assertEquals(1, pageResult.getTotal());  
165 - assertEquals(1, pageResult.getList().size());  
166 - assertPojoEquals(dbRewardActivity, pageResult.getList().get(0), "rules"); 148 + // mock 数据
  149 + RewardActivityDO dbRewardActivity = randomPojo(RewardActivityDO.class, o -> { // 等会查询到
  150 + o.setName("芋艿");
  151 + o.setStatus(CommonStatusEnum.DISABLE.getStatus());
  152 + });
  153 + rewardActivityMapper.insert(dbRewardActivity);
  154 + // 测试 name 不匹配
  155 + rewardActivityMapper.insert(cloneIgnoreId(dbRewardActivity, o -> o.setName("土豆")));
  156 + // 测试 status 不匹配
  157 + rewardActivityMapper.insert(cloneIgnoreId(dbRewardActivity, o -> o.setStatus(CommonStatusEnum.ENABLE.getStatus())));
  158 + // 准备参数
  159 + RewardActivityPageReqVO reqVO = new RewardActivityPageReqVO();
  160 + reqVO.setName("芋艿");
  161 + reqVO.setStatus(CommonStatusEnum.DISABLE.getStatus());
  162 +
  163 + // 调用
  164 + PageResult<RewardActivityDO> pageResult = rewardActivityService.getRewardActivityPage(reqVO);
  165 + // 断言
  166 + assertEquals(1, pageResult.getTotal());
  167 + assertEquals(1, pageResult.getList().size());
  168 + assertPojoEquals(dbRewardActivity, pageResult.getList().get(0), "rules");
167 } 169 }
168 170
169 @Test 171 @Test
170 public void testGetRewardActivities_all() { 172 public void testGetRewardActivities_all() {
171 // mock 数据 173 // mock 数据
172 - RewardActivityDO allActivity = randomPojo(RewardActivityDO.class, o -> o.setStatus(PromotionActivityStatusEnum.RUN.getStatus()) 174 + RewardActivityDO allActivity = randomPojo(RewardActivityDO.class, o -> o.setStatus(CommonStatusEnum.ENABLE.getStatus())
173 .setProductScope(PromotionProductScopeEnum.ALL.getScope())); 175 .setProductScope(PromotionProductScopeEnum.ALL.getScope()));
174 rewardActivityMapper.insert(allActivity); 176 rewardActivityMapper.insert(allActivity);
175 - RewardActivityDO productActivity = randomPojo(RewardActivityDO.class, o -> o.setStatus(PromotionActivityStatusEnum.RUN.getStatus())  
176 - .setProductScope(PromotionProductScopeEnum.SPU.getScope()).setProductSpuIds(asList(1L, 2L))); 177 + RewardActivityDO productActivity = randomPojo(RewardActivityDO.class, o -> o.setStatus(CommonStatusEnum.ENABLE.getStatus())
  178 + .setProductScope(PromotionProductScopeEnum.SPU.getScope()).setProductScopeValues(asList(1L, 2L)));
177 rewardActivityMapper.insert(productActivity); 179 rewardActivityMapper.insert(productActivity);
178 // 准备参数 180 // 准备参数
179 Set<Long> spuIds = asSet(1L, 2L); 181 Set<Long> spuIds = asSet(1L, 2L);
180 182
181 // 调用 TODO getMatchRewardActivities 没有这个方法,但是找到了 getMatchRewardActivityList 183 // 调用 TODO getMatchRewardActivities 没有这个方法,但是找到了 getMatchRewardActivityList
182 - //Map<RewardActivityDO, Set<Long>> matchRewardActivities = rewardActivityService.getMatchRewardActivities(spuIds); 184 + List<RewardActivityMatchRespDTO> matchRewardActivityList = rewardActivityService.getMatchRewardActivityList(spuIds);
183 // 断言 185 // 断言
184 - //assertEquals(matchRewardActivities.size(), 1);  
185 - //Map.Entry<RewardActivityDO, Set<Long>> next = matchRewardActivities.entrySet().iterator().next();  
186 - //assertPojoEquals(next.getKey(), allActivity);  
187 - //assertEquals(next.getValue(), spuIds); 186 + assertEquals(matchRewardActivityList.size(), 1);
  187 + matchRewardActivityList.forEach((activity) -> {
  188 + if (activity.getId().equals(productActivity.getId())) {
  189 + assertPojoEquals(activity, productActivity);
  190 + assertEquals(activity.getProductScopeValues(), asList(1L, 2L));
  191 + } else {
  192 + fail();
  193 + }
  194 + });
188 } 195 }
189 196
190 @Test 197 @Test
191 public void testGetRewardActivities_product() { 198 public void testGetRewardActivities_product() {
192 // mock 数据 199 // mock 数据
193 - RewardActivityDO productActivity01 = randomPojo(RewardActivityDO.class, o -> o.setStatus(PromotionActivityStatusEnum.RUN.getStatus())  
194 - .setProductScope(PromotionProductScopeEnum.SPU.getScope()).setProductSpuIds(asList(1L, 2L))); 200 + RewardActivityDO productActivity01 = randomPojo(RewardActivityDO.class, o -> o.setStatus(CommonStatusEnum.ENABLE.getStatus())
  201 + .setProductScope(PromotionProductScopeEnum.SPU.getScope()).setProductScopeValues(asList(1L, 2L)));
195 rewardActivityMapper.insert(productActivity01); 202 rewardActivityMapper.insert(productActivity01);
196 - RewardActivityDO productActivity02 = randomPojo(RewardActivityDO.class, o -> o.setStatus(PromotionActivityStatusEnum.RUN.getStatus())  
197 - .setProductScope(PromotionProductScopeEnum.SPU.getScope()).setProductSpuIds(singletonList(3L))); 203 + RewardActivityDO productActivity02 = randomPojo(RewardActivityDO.class, o -> o.setStatus(CommonStatusEnum.ENABLE.getStatus())
  204 + .setProductScope(PromotionProductScopeEnum.SPU.getScope()).setProductScopeValues(singletonList(3L)));
198 rewardActivityMapper.insert(productActivity02); 205 rewardActivityMapper.insert(productActivity02);
199 // 准备参数 206 // 准备参数
200 Set<Long> spuIds = asSet(1L, 2L, 3L); 207 Set<Long> spuIds = asSet(1L, 2L, 3L);
201 208
202 // 调用 TODO getMatchRewardActivities 没有这个方法,但是找到了 getMatchRewardActivityList 209 // 调用 TODO getMatchRewardActivities 没有这个方法,但是找到了 getMatchRewardActivityList
203 - //Map<RewardActivityDO, Set<Long>> matchRewardActivities = rewardActivityService.getMatchRewardActivities(spuIds); 210 + List<RewardActivityMatchRespDTO> matchRewardActivityList = rewardActivityService.getMatchRewardActivityList(spuIds);
204 // 断言 211 // 断言
205 - //assertEquals(matchRewardActivities.size(), 2);  
206 - //matchRewardActivities.forEach((activity, activitySpuIds) -> {  
207 - // if (activity.getId().equals(productActivity01.getId())) {  
208 - // assertPojoEquals(activity, productActivity01);  
209 - // assertEquals(activitySpuIds, asSet(1L, 2L));  
210 - // } else if (activity.getId().equals(productActivity02.getId())) {  
211 - // assertPojoEquals(activity, productActivity02);  
212 - // assertEquals(activitySpuIds, asSet(3L));  
213 - // } else {  
214 - // fail();  
215 - // }  
216 - //}); 212 + assertEquals(matchRewardActivityList.size(), 2);
  213 + matchRewardActivityList.forEach((activity) -> {
  214 + if (activity.getId().equals(productActivity01.getId())) {
  215 + assertPojoEquals(activity, productActivity01);
  216 + assertEquals(activity.getProductScopeValues(), asList(1L, 2L));
  217 + } else if (activity.getId().equals(productActivity02.getId())) {
  218 + assertPojoEquals(activity, productActivity02);
  219 + assertEquals(activity.getProductScopeValues(), singletonList(3L));
  220 + } else {
  221 + fail();
  222 + }
  223 + });
217 } 224 }
218 225
219 } 226 }
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 { @@ -28,13 +28,13 @@ public interface TradeOrderApi {
28 */ 28 */
29 TradeOrderRespDTO getOrder(Long id); 29 TradeOrderRespDTO getOrder(Long id);
30 30
31 - // TODO 芋艿:需要优化下;  
32 /** 31 /**
33 * 取消支付订单 32 * 取消支付订单
34 * 33 *
35 - * @param userId 用户编号 34 + * @param userId 用户编号
36 * @param orderId 订单编号 35 * @param orderId 订单编号
  36 + * @param cancelType 取消类型
37 */ 37 */
38 - void cancelPaidOrder(Long userId, Long orderId); 38 + void cancelPaidOrder(Long userId, Long orderId, Integer cancelType);
39 39
40 } 40 }
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 { @@ -35,6 +35,7 @@ public interface ErrorCodeConstants {
35 ErrorCode ORDER_RECEIVE_FAIL_DELIVERY_TYPE_NOT_PICK_UP = new ErrorCode(1_011_000_030, "交易订单自提失败,收货方式不是【用户自提】"); 35 ErrorCode ORDER_RECEIVE_FAIL_DELIVERY_TYPE_NOT_PICK_UP = new ErrorCode(1_011_000_030, "交易订单自提失败,收货方式不是【用户自提】");
36 ErrorCode ORDER_UPDATE_ADDRESS_FAIL_STATUS_NOT_DELIVERED = new ErrorCode(1_011_000_031, "交易订单修改收货地址失败,原因:订单不是【待发货】状态"); 36 ErrorCode ORDER_UPDATE_ADDRESS_FAIL_STATUS_NOT_DELIVERED = new ErrorCode(1_011_000_031, "交易订单修改收货地址失败,原因:订单不是【待发货】状态");
37 ErrorCode ORDER_CREATE_FAIL_EXIST_UNPAID = new ErrorCode(1_011_000_032, "交易订单创建失败,原因:存在未付款订单"); 37 ErrorCode ORDER_CREATE_FAIL_EXIST_UNPAID = new ErrorCode(1_011_000_032, "交易订单创建失败,原因:存在未付款订单");
  38 + ErrorCode ORDER_CANCEL_PAID_FAIL = new ErrorCode(1_011_000_033, "交易订单取消支付失败,原因:订单不是【{}】状态");
38 39
39 // ========== After Sale 模块 1-011-000-100 ========== 40 // ========== After Sale 模块 1-011-000-100 ==========
40 ErrorCode AFTER_SALE_NOT_FOUND = new ErrorCode(1_011_000_100, "售后单不存在"); 41 ErrorCode AFTER_SALE_NOT_FOUND = new ErrorCode(1_011_000_100, "售后单不存在");
@@ -59,6 +60,8 @@ public interface ErrorCodeConstants { @@ -59,6 +60,8 @@ public interface ErrorCodeConstants {
59 ErrorCode PRICE_CALCULATE_DELIVERY_PRICE_TEMPLATE_NOT_FOUND = new ErrorCode(1_011_003_002, "计算快递运费异常,找不到对应的运费模板"); 60 ErrorCode PRICE_CALCULATE_DELIVERY_PRICE_TEMPLATE_NOT_FOUND = new ErrorCode(1_011_003_002, "计算快递运费异常,找不到对应的运费模板");
60 ErrorCode PRICE_CALCULATE_COUPON_NOT_MATCH_NORMAL_ORDER = new ErrorCode(1_011_003_004, "参与秒杀、拼团、砍价的营销商品,无法使用优惠劵"); 61 ErrorCode PRICE_CALCULATE_COUPON_NOT_MATCH_NORMAL_ORDER = new ErrorCode(1_011_003_004, "参与秒杀、拼团、砍价的营销商品,无法使用优惠劵");
61 ErrorCode PRICE_CALCULATE_SECKILL_TOTAL_LIMIT_COUNT = new ErrorCode(1_011_003_005, "参与秒杀的商品,超过了秒杀总限购数量"); 62 ErrorCode PRICE_CALCULATE_SECKILL_TOTAL_LIMIT_COUNT = new ErrorCode(1_011_003_005, "参与秒杀的商品,超过了秒杀总限购数量");
  63 + ErrorCode PRICE_CALCULATE_DELIVERY_PRICE_TYPE_ILLEGAL = new ErrorCode(1_011_003_006, "计算快递运费异常,配送方式不匹配");
  64 + ErrorCode PRICE_CALCULATE_COUPON_PRICE_TOO_MUCH = new ErrorCode(1_011_003_007, "该优惠劵无法使用,原因:优惠金额超过订单金额");
62 65
63 // ========== 物流 Express 模块 1-011-004-000 ========== 66 // ========== 物流 Express 模块 1-011-004-000 ==========
64 ErrorCode EXPRESS_NOT_EXISTS = new ErrorCode(1_011_004_000, "快递公司不存在"); 67 ErrorCode EXPRESS_NOT_EXISTS = new ErrorCode(1_011_004_000, "快递公司不存在");
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 { @@ -17,7 +17,8 @@ public enum TradeOrderCancelTypeEnum implements IntArrayValuable {
17 17
18 PAY_TIMEOUT(10, "超时未支付"), 18 PAY_TIMEOUT(10, "超时未支付"),
19 AFTER_SALE_CLOSE(20, "退款关闭"), 19 AFTER_SALE_CLOSE(20, "退款关闭"),
20 - MEMBER_CANCEL(30, "买家取消"); 20 + MEMBER_CANCEL(30, "买家取消"),
  21 + COMBINATION_CLOSE(40, "拼团关闭");
21 22
22 public static final int[] ARRAYS = Arrays.stream(values()).mapToInt(TradeOrderCancelTypeEnum::getType).toArray(); 23 public static final int[] ARRAYS = Arrays.stream(values()).mapToInt(TradeOrderCancelTypeEnum::getType).toArray();
23 24
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; @@ -4,10 +4,10 @@ import cn.iocoder.yudao.module.trade.api.order.dto.TradeOrderRespDTO;
4 import cn.iocoder.yudao.module.trade.convert.order.TradeOrderConvert; 4 import cn.iocoder.yudao.module.trade.convert.order.TradeOrderConvert;
5 import cn.iocoder.yudao.module.trade.service.order.TradeOrderQueryService; 5 import cn.iocoder.yudao.module.trade.service.order.TradeOrderQueryService;
6 import cn.iocoder.yudao.module.trade.service.order.TradeOrderUpdateService; 6 import cn.iocoder.yudao.module.trade.service.order.TradeOrderUpdateService;
  7 +import jakarta.annotation.Resource;
7 import org.springframework.stereotype.Service; 8 import org.springframework.stereotype.Service;
8 import org.springframework.validation.annotation.Validated; 9 import org.springframework.validation.annotation.Validated;
9 10
10 -import jakarta.annotation.Resource;  
11 import java.util.Collection; 11 import java.util.Collection;
12 import java.util.List; 12 import java.util.List;
13 13
@@ -36,8 +36,8 @@ public class TradeOrderApiImpl implements TradeOrderApi { @@ -36,8 +36,8 @@ public class TradeOrderApiImpl implements TradeOrderApi {
36 } 36 }
37 37
38 @Override 38 @Override
39 - public void cancelPaidOrder(Long userId, Long orderId) {  
40 - tradeOrderUpdateService.cancelPaidOrder(userId, orderId); 39 + public void cancelPaidOrder(Long userId, Long orderId, Integer cancelType) {
  40 + tradeOrderUpdateService.cancelPaidOrder(userId, orderId, cancelType);
41 } 41 }
42 42
43 } 43 }
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; @@ -2,6 +2,7 @@ package cn.iocoder.yudao.module.trade.dal.dataobject.order;
2 2
3 import cn.iocoder.yudao.framework.common.enums.TerminalEnum; 3 import cn.iocoder.yudao.framework.common.enums.TerminalEnum;
4 import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO; 4 import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO;
  5 +import cn.iocoder.yudao.framework.mybatis.core.type.LongListTypeHandler;
5 import cn.iocoder.yudao.module.member.api.user.dto.MemberUserRespDTO; 6 import cn.iocoder.yudao.module.member.api.user.dto.MemberUserRespDTO;
6 import cn.iocoder.yudao.module.trade.dal.dataobject.brokerage.BrokerageUserDO; 7 import cn.iocoder.yudao.module.trade.dal.dataobject.brokerage.BrokerageUserDO;
7 import cn.iocoder.yudao.module.trade.dal.dataobject.delivery.DeliveryExpressDO; 8 import cn.iocoder.yudao.module.trade.dal.dataobject.delivery.DeliveryExpressDO;
@@ -12,10 +13,14 @@ import cn.iocoder.yudao.module.trade.enums.order.TradeOrderRefundStatusEnum; @@ -12,10 +13,14 @@ import cn.iocoder.yudao.module.trade.enums.order.TradeOrderRefundStatusEnum;
12 import cn.iocoder.yudao.module.trade.enums.order.TradeOrderStatusEnum; 13 import cn.iocoder.yudao.module.trade.enums.order.TradeOrderStatusEnum;
13 import cn.iocoder.yudao.module.trade.enums.order.TradeOrderTypeEnum; 14 import cn.iocoder.yudao.module.trade.enums.order.TradeOrderTypeEnum;
14 import com.baomidou.mybatisplus.annotation.KeySequence; 15 import com.baomidou.mybatisplus.annotation.KeySequence;
  16 +import com.baomidou.mybatisplus.annotation.TableField;
15 import com.baomidou.mybatisplus.annotation.TableName; 17 import com.baomidou.mybatisplus.annotation.TableName;
  18 +import com.baomidou.mybatisplus.extension.handlers.JacksonTypeHandler;
16 import lombok.*; 19 import lombok.*;
17 20
18 import java.time.LocalDateTime; 21 import java.time.LocalDateTime;
  22 +import java.util.List;
  23 +import java.util.Map;
19 24
20 /** 25 /**
21 * 交易订单 DO 26 * 交易订单 DO
@@ -292,6 +297,24 @@ public class TradeOrderDO extends BaseDO { @@ -292,6 +297,24 @@ public class TradeOrderDO extends BaseDO {
292 private Integer vipPrice; 297 private Integer vipPrice;
293 298
294 /** 299 /**
  300 + * 赠送的优惠劵
  301 + *
  302 + * key: 优惠劵模版编号
  303 + * value:对应的优惠券数量
  304 + *
  305 + * 目的:用于订单支付后赠送优惠券
  306 + */
  307 + @TableField(typeHandler = JacksonTypeHandler.class)
  308 + private Map<Long, Integer> giveCouponTemplateCounts;
  309 + /**
  310 + * 赠送的优惠劵编号
  311 + *
  312 + * 目的:用于后续取消或者售后订单时,需要扣减赠送
  313 + */
  314 + @TableField(typeHandler = LongListTypeHandler.class)
  315 + private List<Long> giveCouponIds;
  316 +
  317 + /**
295 * 秒杀活动编号 318 * 秒杀活动编号
296 * 319 *
297 * 关联 SeckillActivityDO 的 id 字段 320 * 关联 SeckillActivityDO 的 id 字段
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 { @@ -268,11 +268,6 @@ public class BrokerageUserServiceImpl implements BrokerageUserService {
268 return false; 268 return false;
269 } 269 }
270 270
271 - // 校验分佣模式:仅可后台手动设置推广员  
272 - // if (BrokerageEnabledConditionEnum.ADMIN.getCondition().equals(tradeConfig.getBrokerageEnabledCondition())) {  
273 - // throw exception(BROKERAGE_BIND_CONDITION_ADMIN);  
274 - // }  
275 -  
276 // 校验分销关系绑定模式 271 // 校验分销关系绑定模式
277 if (BrokerageBindModeEnum.REGISTER.getMode().equals(tradeConfig.getBrokerageBindMode())) { 272 if (BrokerageBindModeEnum.REGISTER.getMode().equals(tradeConfig.getBrokerageBindMode())) {
278 // 判断是否为新用户:注册时间在 30 秒内的,都算新用户 273 // 判断是否为新用户:注册时间在 30 秒内的,都算新用户
yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/service/order/TradeOrderUpdateService.java
1 package cn.iocoder.yudao.module.trade.service.order; 1 package cn.iocoder.yudao.module.trade.service.order;
2 2
3 -import cn.iocoder.yudao.framework.common.enums.TerminalEnum;  
4 import cn.iocoder.yudao.module.trade.controller.admin.order.vo.TradeOrderDeliveryReqVO; 3 import cn.iocoder.yudao.module.trade.controller.admin.order.vo.TradeOrderDeliveryReqVO;
5 import cn.iocoder.yudao.module.trade.controller.admin.order.vo.TradeOrderRemarkReqVO; 4 import cn.iocoder.yudao.module.trade.controller.admin.order.vo.TradeOrderRemarkReqVO;
6 import cn.iocoder.yudao.module.trade.controller.admin.order.vo.TradeOrderUpdateAddressReqVO; 5 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 @@ -10,9 +9,10 @@ import cn.iocoder.yudao.module.trade.controller.app.order.vo.AppTradeOrderSettle
10 import cn.iocoder.yudao.module.trade.controller.app.order.vo.AppTradeOrderSettlementRespVO; 9 import cn.iocoder.yudao.module.trade.controller.app.order.vo.AppTradeOrderSettlementRespVO;
11 import cn.iocoder.yudao.module.trade.controller.app.order.vo.item.AppTradeOrderItemCommentCreateReqVO; 10 import cn.iocoder.yudao.module.trade.controller.app.order.vo.item.AppTradeOrderItemCommentCreateReqVO;
12 import cn.iocoder.yudao.module.trade.dal.dataobject.order.TradeOrderDO; 11 import cn.iocoder.yudao.module.trade.dal.dataobject.order.TradeOrderDO;
13 -  
14 import jakarta.validation.constraints.NotNull; 12 import jakarta.validation.constraints.NotNull;
15 13
  14 +import java.util.List;
  15 +
16 /** 16 /**
17 * 交易订单【写】Service 接口 17 * 交易订单【写】Service 接口
18 * 18 *
@@ -187,13 +187,22 @@ public interface TradeOrderUpdateService { @@ -187,13 +187,22 @@ public interface TradeOrderUpdateService {
187 */ 187 */
188 void updateOrderCombinationInfo(Long orderId, Long activityId, Long combinationRecordId, Long headId); 188 void updateOrderCombinationInfo(Long orderId, Long activityId, Long combinationRecordId, Long headId);
189 189
190 - // TODO 芋艿:拼团取消,不调这个接口哈;  
191 /** 190 /**
192 * 取消支付订单 191 * 取消支付订单
193 * 192 *
194 - * @param userId 用户编号  
195 - * @param orderId 订单编号 193 + * @param userId 用户编号
  194 + * @param orderId 订单编号
  195 + * @param cancelType 取消类型
  196 + */
  197 + void cancelPaidOrder(Long userId, Long orderId, Integer cancelType);
  198 +
  199 + /**
  200 + * 更新下单赠送的优惠券编号到订单
  201 + *
  202 + * @param userId 用户编号
  203 + * @param orderId 订单编号
  204 + * @param giveCouponIds 赠送的优惠券编号列表
196 */ 205 */
197 - void cancelPaidOrder(Long userId, Long orderId); 206 + void updateOrderGiveCouponIds(Long userId, Long orderId, List<Long> giveCouponIds);
198 207
199 } 208 }
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; @@ -18,6 +18,8 @@ import cn.iocoder.yudao.module.member.api.address.dto.MemberAddressRespDTO;
18 import cn.iocoder.yudao.module.pay.api.order.PayOrderApi; 18 import cn.iocoder.yudao.module.pay.api.order.PayOrderApi;
19 import cn.iocoder.yudao.module.pay.api.order.dto.PayOrderCreateReqDTO; 19 import cn.iocoder.yudao.module.pay.api.order.dto.PayOrderCreateReqDTO;
20 import cn.iocoder.yudao.module.pay.api.order.dto.PayOrderRespDTO; 20 import cn.iocoder.yudao.module.pay.api.order.dto.PayOrderRespDTO;
  21 +import cn.iocoder.yudao.module.pay.api.refund.PayRefundApi;
  22 +import cn.iocoder.yudao.module.pay.api.refund.dto.PayRefundCreateReqDTO;
21 import cn.iocoder.yudao.module.pay.enums.order.PayOrderStatusEnum; 23 import cn.iocoder.yudao.module.pay.enums.order.PayOrderStatusEnum;
22 import cn.iocoder.yudao.module.product.api.comment.ProductCommentApi; 24 import cn.iocoder.yudao.module.product.api.comment.ProductCommentApi;
23 import cn.iocoder.yudao.module.product.api.comment.dto.ProductCommentCreateReqDTO; 25 import cn.iocoder.yudao.module.product.api.comment.dto.ProductCommentCreateReqDTO;
@@ -111,6 +113,8 @@ public class TradeOrderUpdateServiceImpl implements TradeOrderUpdateService { @@ -111,6 +113,8 @@ public class TradeOrderUpdateServiceImpl implements TradeOrderUpdateService {
111 private ProductCommentApi productCommentApi; 113 private ProductCommentApi productCommentApi;
112 @Resource 114 @Resource
113 public SocialClientApi socialClientApi; 115 public SocialClientApi socialClientApi;
  116 + @Resource
  117 + public PayRefundApi payRefundApi;
114 118
115 @Resource 119 @Resource
116 private TradeOrderProperties tradeOrderProperties; 120 private TradeOrderProperties tradeOrderProperties;
@@ -197,6 +201,8 @@ public class TradeOrderUpdateServiceImpl implements TradeOrderUpdateService { @@ -197,6 +201,8 @@ public class TradeOrderUpdateServiceImpl implements TradeOrderUpdateService {
197 order.setRefundStatus(TradeOrderRefundStatusEnum.NONE.getStatus()); 201 order.setRefundStatus(TradeOrderRefundStatusEnum.NONE.getStatus());
198 order.setProductCount(getSumValue(calculateRespBO.getItems(), TradePriceCalculateRespBO.OrderItem::getCount, Integer::sum)); 202 order.setProductCount(getSumValue(calculateRespBO.getItems(), TradePriceCalculateRespBO.OrderItem::getCount, Integer::sum));
199 order.setUserIp(getClientIP()).setTerminal(getTerminal()); 203 order.setUserIp(getClientIP()).setTerminal(getTerminal());
  204 + // 使用 + 赠送优惠券
  205 + order.setGiveCouponTemplateCounts(calculateRespBO.getGiveCouponTemplateCounts());
200 // 支付 + 退款信息 206 // 支付 + 退款信息
201 order.setAdjustPrice(0).setPayStatus(false); 207 order.setAdjustPrice(0).setPayStatus(false);
202 order.setRefundStatus(TradeOrderRefundStatusEnum.NONE.getStatus()).setRefundPrice(0); 208 order.setRefundStatus(TradeOrderRefundStatusEnum.NONE.getStatus()).setRefundPrice(0);
@@ -854,15 +860,46 @@ public class TradeOrderUpdateServiceImpl implements TradeOrderUpdateService { @@ -854,15 +860,46 @@ public class TradeOrderUpdateServiceImpl implements TradeOrderUpdateService {
854 860
855 @Override 861 @Override
856 @Transactional(rollbackFor = Exception.class) 862 @Transactional(rollbackFor = Exception.class)
857 - public void cancelPaidOrder(Long userId, Long orderId) {  
858 - // TODO @puhui999:需要校验状态;已支付的情况下,才可以。 863 + public void cancelPaidOrder(Long userId, Long orderId, Integer cancelType) {
  864 + // 1.1 这里校验下 cancelType 只允许拼团关闭;
  865 + if (ObjUtil.notEqual(TradeOrderCancelTypeEnum.COMBINATION_CLOSE.getType(), cancelType)) {
  866 + return;
  867 + }
  868 + // 1.2 检验订单存在
  869 + TradeOrderDO order = tradeOrderMapper.selectOrderByIdAndUserId(orderId, userId);
  870 + if (order == null) {
  871 + throw exception(ORDER_NOT_FOUND);
  872 + }
  873 +
  874 + // 1.3 校验订单是否支付
  875 + if (!order.getPayStatus()) {
  876 + throw exception(ORDER_CANCEL_PAID_FAIL, "已支付");
  877 + }
  878 + // 1.3 校验订单是否已退款
  879 + if (ObjUtil.equal(TradeOrderRefundStatusEnum.NONE.getStatus(), order.getRefundStatus())) {
  880 + throw exception(ORDER_CANCEL_PAID_FAIL, "未退款");
  881 + }
  882 +
  883 + // 2.1 取消订单
  884 + cancelOrder0(order, TradeOrderCancelTypeEnum.COMBINATION_CLOSE);
  885 + // 2.2 创建退款单
  886 + payRefundApi.createRefund(new PayRefundCreateReqDTO()
  887 + .setAppKey(tradeOrderProperties.getPayAppKey()).setUserIp(getClientIP()) // 支付应用
  888 + .setMerchantOrderId(String.valueOf(order.getId())) // 支付单号
  889 + .setMerchantRefundId(String.valueOf(order.getId()))
  890 + .setReason(TradeOrderCancelTypeEnum.COMBINATION_CLOSE.getName()).setPrice(order.getPayPrice()));// 价格信息
  891 + }
  892 +
  893 + @Override
  894 + public void updateOrderGiveCouponIds(Long userId, Long orderId, List<Long> giveCouponIds) {
  895 + // 1. 检验订单存在
859 TradeOrderDO order = tradeOrderMapper.selectOrderByIdAndUserId(orderId, userId); 896 TradeOrderDO order = tradeOrderMapper.selectOrderByIdAndUserId(orderId, userId);
860 if (order == null) { 897 if (order == null) {
861 throw exception(ORDER_NOT_FOUND); 898 throw exception(ORDER_NOT_FOUND);
862 } 899 }
863 - cancelOrder0(order, TradeOrderCancelTypeEnum.MEMBER_CANCEL);  
864 900
865 - // TODO @puhui999:需要退款 901 + // 2. 更新订单赠送的优惠券编号列表
  902 + tradeOrderMapper.updateById(new TradeOrderDO().setId(orderId).setGiveCouponIds(giveCouponIds));
866 } 903 }
867 904
868 /** 905 /**
yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/service/order/handler/TradeCouponOrderHandler.java
1 package cn.iocoder.yudao.module.trade.service.order.handler; 1 package cn.iocoder.yudao.module.trade.service.order.handler;
2 2
  3 +import cn.hutool.core.collection.CollUtil;
3 import cn.iocoder.yudao.module.promotion.api.coupon.CouponApi; 4 import cn.iocoder.yudao.module.promotion.api.coupon.CouponApi;
4 import cn.iocoder.yudao.module.promotion.api.coupon.dto.CouponUseReqDTO; 5 import cn.iocoder.yudao.module.promotion.api.coupon.dto.CouponUseReqDTO;
5 import cn.iocoder.yudao.module.trade.dal.dataobject.order.TradeOrderDO; 6 import cn.iocoder.yudao.module.trade.dal.dataobject.order.TradeOrderDO;
6 import cn.iocoder.yudao.module.trade.dal.dataobject.order.TradeOrderItemDO; 7 import cn.iocoder.yudao.module.trade.dal.dataobject.order.TradeOrderItemDO;
  8 +import cn.iocoder.yudao.module.trade.service.order.TradeOrderQueryService;
  9 +import cn.iocoder.yudao.module.trade.service.order.TradeOrderUpdateService;
  10 +import jakarta.annotation.Resource;
  11 +import org.springframework.context.annotation.Lazy;
7 import org.springframework.stereotype.Component; 12 import org.springframework.stereotype.Component;
8 13
9 -import jakarta.annotation.Resource;  
10 import java.util.List; 14 import java.util.List;
11 15
12 /** 16 /**
@@ -18,6 +22,12 @@ import java.util.List; @@ -18,6 +22,12 @@ import java.util.List;
18 public class TradeCouponOrderHandler implements TradeOrderHandler { 22 public class TradeCouponOrderHandler implements TradeOrderHandler {
19 23
20 @Resource 24 @Resource
  25 + @Lazy // 延迟加载,避免循环依赖
  26 + private TradeOrderUpdateService orderUpdateService;
  27 + @Resource
  28 + private TradeOrderQueryService orderQueryService;
  29 +
  30 + @Resource
21 private CouponApi couponApi; 31 private CouponApi couponApi;
22 32
23 @Override 33 @Override
@@ -31,12 +41,30 @@ public class TradeCouponOrderHandler implements TradeOrderHandler { @@ -31,12 +41,30 @@ public class TradeCouponOrderHandler implements TradeOrderHandler {
31 } 41 }
32 42
33 @Override 43 @Override
  44 + public void afterPayOrder(TradeOrderDO order, List<TradeOrderItemDO> orderItems) {
  45 + if (CollUtil.isEmpty(order.getGiveCouponTemplateCounts())) {
  46 + return;
  47 + }
  48 + // 赠送优惠券
  49 + List<Long> couponIds = couponApi.takeCouponsByAdmin(order.getGiveCouponTemplateCounts(), order.getUserId());
  50 + if (CollUtil.isEmpty(couponIds)) {
  51 + return;
  52 + }
  53 + orderUpdateService.updateOrderGiveCouponIds(order.getUserId(), order.getId(), couponIds);
  54 + }
  55 +
  56 + @Override
34 public void afterCancelOrder(TradeOrderDO order, List<TradeOrderItemDO> orderItems) { 57 public void afterCancelOrder(TradeOrderDO order, List<TradeOrderItemDO> orderItems) {
35 - if (order.getCouponId() == null || order.getCouponId() <= 0) { 58 + // 情况一:退还订单使用的优惠券
  59 + if (order.getCouponId() != null && order.getCouponId() > 0) {
  60 + // 退回优惠劵
  61 + couponApi.returnUsedCoupon(order.getCouponId());
  62 + }
  63 + // 情况二:收回赠送的优惠券
  64 + if (CollUtil.isEmpty(order.getGiveCouponIds())) {
36 return; 65 return;
37 } 66 }
38 - // 退回优惠劵  
39 - couponApi.returnUsedCoupon(order.getCouponId()); 67 + couponApi.invalidateCouponsByAdmin(order.getGiveCouponIds(), order.getUserId());
40 } 68 }
41 69
42 } 70 }
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; @@ -6,6 +6,7 @@ import cn.iocoder.yudao.module.trade.enums.order.TradeOrderTypeEnum;
6 import lombok.Data; 6 import lombok.Data;
7 7
8 import java.util.List; 8 import java.util.List;
  9 +import java.util.Map;
9 10
10 /** 11 /**
11 * 价格计算 Response BO 12 * 价格计算 Response BO
@@ -68,6 +69,21 @@ public class TradePriceCalculateRespBO { @@ -68,6 +69,21 @@ public class TradePriceCalculateRespBO {
68 private Long bargainActivityId; 69 private Long bargainActivityId;
69 70
70 /** 71 /**
  72 + * 是否包邮
  73 + */
  74 + private Boolean freeDelivery;
  75 +
  76 + /**
  77 + * 赠送的优惠劵
  78 + *
  79 + * key: 优惠劵模版编号
  80 + * value:对应的优惠券数量
  81 + *
  82 + * 目的:用于订单支付后赠送优惠券
  83 + */
  84 + private Map<Long, Integer> giveCouponTemplateCounts;
  85 +
  86 + /**
71 * 订单价格 87 * 订单价格
72 */ 88 */
73 @Data 89 @Data
@@ -213,8 +229,19 @@ public class TradePriceCalculateRespBO { @@ -213,8 +229,19 @@ public class TradePriceCalculateRespBO {
213 */ 229 */
214 private Long categoryId; 230 private Long categoryId;
215 231
  232 + // ========== 物流相关字段 =========
  233 +
216 /** 234 /**
217 - * 运费模板 Id 235 + * 配送方式数组
  236 + *
  237 + * 对应 DeliveryTypeEnum 枚举
  238 + */
  239 + private List<Integer> deliveryTypes;
  240 +
  241 + /**
  242 + * 物流配置模板编号
  243 + *
  244 + * 对应 TradeDeliveryExpressTemplateDO 的 id 编号
218 */ 245 */
219 private Long deliveryTemplateId; 246 private Long deliveryTemplateId;
220 247
@@ -234,7 +261,7 @@ public class TradePriceCalculateRespBO { @@ -234,7 +261,7 @@ public class TradePriceCalculateRespBO {
234 private List<ProductPropertyValueDetailRespDTO> properties; 261 private List<ProductPropertyValueDetailRespDTO> properties;
235 262
236 /** 263 /**
237 - * 使用的积分 264 + * 赠送的积分
238 */ 265 */
239 private Integer givePoint; 266 private Integer givePoint;
240 267
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. @@ -25,6 +25,7 @@ import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.
25 import static cn.iocoder.yudao.module.promotion.enums.ErrorCodeConstants.COUPON_NO_MATCH_MIN_PRICE; 25 import static cn.iocoder.yudao.module.promotion.enums.ErrorCodeConstants.COUPON_NO_MATCH_MIN_PRICE;
26 import static cn.iocoder.yudao.module.promotion.enums.ErrorCodeConstants.COUPON_NO_MATCH_SPU; 26 import static cn.iocoder.yudao.module.promotion.enums.ErrorCodeConstants.COUPON_NO_MATCH_SPU;
27 import static cn.iocoder.yudao.module.trade.enums.ErrorCodeConstants.PRICE_CALCULATE_COUPON_NOT_MATCH_NORMAL_ORDER; 27 import static cn.iocoder.yudao.module.trade.enums.ErrorCodeConstants.PRICE_CALCULATE_COUPON_NOT_MATCH_NORMAL_ORDER;
  28 +import static cn.iocoder.yudao.module.trade.enums.ErrorCodeConstants.PRICE_CALCULATE_COUPON_PRICE_TOO_MUCH;
28 29
29 /** 30 /**
30 * 优惠劵的 {@link TradePriceCalculator} 实现类 31 * 优惠劵的 {@link TradePriceCalculator} 实现类
@@ -65,8 +66,9 @@ public class TradeCouponPriceCalculator implements TradePriceCalculator { @@ -65,8 +66,9 @@ public class TradeCouponPriceCalculator implements TradePriceCalculator {
65 66
66 // 3.1 计算可以优惠的金额 67 // 3.1 计算可以优惠的金额
67 Integer couponPrice = getCouponPrice(coupon, totalPayPrice); 68 Integer couponPrice = getCouponPrice(coupon, totalPayPrice);
68 - Assert.isTrue(couponPrice < totalPayPrice,  
69 - "优惠劵({}) 的优惠金额({}),不能大于订单总金额({})", coupon.getId(), couponPrice, totalPayPrice); 69 + if (couponPrice <= totalPayPrice) {
  70 + throw exception(PRICE_CALCULATE_COUPON_PRICE_TOO_MUCH);
  71 + }
70 // 3.2 计算分摊的优惠金额 72 // 3.2 计算分摊的优惠金额
71 List<Integer> divideCouponPrices = TradePriceCalculatorHelper.dividePrice(orderItems, couponPrice); 73 List<Integer> divideCouponPrices = TradePriceCalculatorHelper.dividePrice(orderItems, couponPrice);
72 74
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; @@ -4,6 +4,7 @@ import cn.hutool.core.collection.CollUtil;
4 import cn.hutool.core.lang.Assert; 4 import cn.hutool.core.lang.Assert;
5 import cn.hutool.core.util.StrUtil; 5 import cn.hutool.core.util.StrUtil;
6 import cn.iocoder.yudao.framework.common.enums.CommonStatusEnum; 6 import cn.iocoder.yudao.framework.common.enums.CommonStatusEnum;
  7 +import cn.iocoder.yudao.framework.common.util.collection.CollectionUtils;
7 import cn.iocoder.yudao.module.member.api.address.MemberAddressApi; 8 import cn.iocoder.yudao.module.member.api.address.MemberAddressApi;
8 import cn.iocoder.yudao.module.member.api.address.dto.MemberAddressRespDTO; 9 import cn.iocoder.yudao.module.member.api.address.dto.MemberAddressRespDTO;
9 import cn.iocoder.yudao.module.trade.dal.dataobject.config.TradeConfigDO; 10 import cn.iocoder.yudao.module.trade.dal.dataobject.config.TradeConfigDO;
@@ -17,11 +18,11 @@ import cn.iocoder.yudao.module.trade.service.delivery.bo.DeliveryExpressTemplate @@ -17,11 +18,11 @@ import cn.iocoder.yudao.module.trade.service.delivery.bo.DeliveryExpressTemplate
17 import cn.iocoder.yudao.module.trade.service.price.bo.TradePriceCalculateReqBO; 18 import cn.iocoder.yudao.module.trade.service.price.bo.TradePriceCalculateReqBO;
18 import cn.iocoder.yudao.module.trade.service.price.bo.TradePriceCalculateRespBO; 19 import cn.iocoder.yudao.module.trade.service.price.bo.TradePriceCalculateRespBO;
19 import cn.iocoder.yudao.module.trade.service.price.bo.TradePriceCalculateRespBO.OrderItem; 20 import cn.iocoder.yudao.module.trade.service.price.bo.TradePriceCalculateRespBO.OrderItem;
  21 +import jakarta.annotation.Resource;
20 import lombok.extern.slf4j.Slf4j; 22 import lombok.extern.slf4j.Slf4j;
21 import org.springframework.core.annotation.Order; 23 import org.springframework.core.annotation.Order;
22 import org.springframework.stereotype.Component; 24 import org.springframework.stereotype.Component;
23 25
24 -import jakarta.annotation.Resource;  
25 import java.util.List; 26 import java.util.List;
26 import java.util.Map; 27 import java.util.Map;
27 import java.util.Set; 28 import java.util.Set;
@@ -55,7 +56,11 @@ public class TradeDeliveryPriceCalculator implements TradePriceCalculator { @@ -55,7 +56,11 @@ public class TradeDeliveryPriceCalculator implements TradePriceCalculator {
55 if (param.getDeliveryType() == null) { 56 if (param.getDeliveryType() == null) {
56 return; 57 return;
57 } 58 }
58 - // TODO @puhui999:需要校验,是不是存在商品不能门店自提,或者不能快递发货的情况。就是说,配送方式不匹配哈 59 + // 校验是不是存在商品不能门店自提,或者不能快递发货的情况。就是说,配送方式不匹配哈
  60 + if (CollectionUtils.anyMatch(result.getItems(), item -> !item.getDeliveryTypes().contains(param.getDeliveryType()))) {
  61 + throw exception(PRICE_CALCULATE_DELIVERY_PRICE_TYPE_ILLEGAL);
  62 + }
  63 +
59 if (DeliveryTypeEnum.PICK_UP.getType().equals(param.getDeliveryType())) { 64 if (DeliveryTypeEnum.PICK_UP.getType().equals(param.getDeliveryType())) {
60 calculateByPickUp(param); 65 calculateByPickUp(param);
61 } else if (DeliveryTypeEnum.EXPRESS.getType().equals(param.getDeliveryType())) { 66 } else if (DeliveryTypeEnum.EXPRESS.getType().equals(param.getDeliveryType())) {
@@ -90,7 +95,12 @@ public class TradeDeliveryPriceCalculator implements TradePriceCalculator { @@ -90,7 +95,12 @@ public class TradeDeliveryPriceCalculator implements TradePriceCalculator {
90 return; 95 return;
91 } 96 }
92 97
93 - // 情况二:快递模版 98 + // 情况二:活动包邮
  99 + if (Boolean.TRUE.equals(result.getFreeDelivery())) {
  100 + return;
  101 + }
  102 +
  103 + // 情况三:快递模版
94 // 2.1 过滤出已选中的商品 SKU 104 // 2.1 过滤出已选中的商品 SKU
95 List<OrderItem> selectedItem = filterList(result.getItems(), OrderItem::getSelected); 105 List<OrderItem> selectedItem = filterList(result.getItems(), OrderItem::getSelected);
96 Set<Long> deliveryTemplateIds = convertSet(selectedItem, OrderItem::getDeliveryTemplateId); 106 Set<Long> deliveryTemplateIds = convertSet(selectedItem, OrderItem::getDeliveryTemplateId);
@@ -124,7 +134,7 @@ public class TradeDeliveryPriceCalculator implements TradePriceCalculator { @@ -124,7 +134,7 @@ public class TradeDeliveryPriceCalculator implements TradePriceCalculator {
124 Map<Long, List<OrderItem>> template2ItemMap = convertMultiMap(selectedSkus, OrderItem::getDeliveryTemplateId); 134 Map<Long, List<OrderItem>> template2ItemMap = convertMultiMap(selectedSkus, OrderItem::getDeliveryTemplateId);
125 // 依次计算快递运费 135 // 依次计算快递运费
126 for (Map.Entry<Long, List<OrderItem>> entry : template2ItemMap.entrySet()) { 136 for (Map.Entry<Long, List<OrderItem>> entry : template2ItemMap.entrySet()) {
127 - Long templateId = entry.getKey(); 137 + Long templateId = entry.getKey();
128 List<OrderItem> orderItems = entry.getValue(); 138 List<OrderItem> orderItems = entry.getValue();
129 DeliveryExpressTemplateRespBO templateBO = expressTemplateMap.get(templateId); 139 DeliveryExpressTemplateRespBO templateBO = expressTemplateMap.get(templateId);
130 if (templateBO == null) { 140 if (templateBO == null) {
@@ -144,8 +154,8 @@ public class TradeDeliveryPriceCalculator implements TradePriceCalculator { @@ -144,8 +154,8 @@ public class TradeDeliveryPriceCalculator implements TradePriceCalculator {
144 /** 154 /**
145 * 按配送方式来计算运费 155 * 按配送方式来计算运费
146 * 156 *
147 - * @param orderItems SKU 商品项目  
148 - * @param chargeMode 配送计费方式 157 + * @param orderItems SKU 商品项目
  158 + * @param chargeMode 配送计费方式
149 * @param templateCharge 快递运费配置 159 * @param templateCharge 快递运费配置
150 */ 160 */
151 private void calculateExpressFeeByChargeMode(List<OrderItem> orderItems, Integer chargeMode, 161 private void calculateExpressFeeByChargeMode(List<OrderItem> orderItems, Integer chargeMode,
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; @@ -11,6 +11,7 @@ import cn.iocoder.yudao.module.trade.service.price.bo.TradePriceCalculateReqBO;
11 import cn.iocoder.yudao.module.trade.service.price.bo.TradePriceCalculateRespBO; 11 import cn.iocoder.yudao.module.trade.service.price.bo.TradePriceCalculateRespBO;
12 12
13 import java.util.ArrayList; 13 import java.util.ArrayList;
  14 +import java.util.LinkedHashMap;
14 import java.util.List; 15 import java.util.List;
15 import java.util.Map; 16 import java.util.Map;
16 17
@@ -31,8 +32,7 @@ public class TradePriceCalculatorHelper { @@ -31,8 +32,7 @@ public class TradePriceCalculatorHelper {
31 List<ProductSpuRespDTO> spuList, List<ProductSkuRespDTO> skuList) { 32 List<ProductSpuRespDTO> spuList, List<ProductSkuRespDTO> skuList) {
32 // 创建 PriceCalculateRespDTO 对象 33 // 创建 PriceCalculateRespDTO 对象
33 TradePriceCalculateRespBO result = new TradePriceCalculateRespBO(); 34 TradePriceCalculateRespBO result = new TradePriceCalculateRespBO();
34 - result.setType(getOrderType(param));  
35 - result.setPromotions(new ArrayList<>()); 35 + result.setType(getOrderType(param)).setPromotions(new ArrayList<>()).setGiveCouponTemplateCounts(new LinkedHashMap<>());
36 36
37 // 创建它的 OrderItem 属性 37 // 创建它的 OrderItem 属性
38 result.setItems(new ArrayList<>(param.getItems().size())); 38 result.setItems(new ArrayList<>(param.getItems().size()));
@@ -60,7 +60,7 @@ public class TradePriceCalculatorHelper { @@ -60,7 +60,7 @@ public class TradePriceCalculatorHelper {
60 .setWeight(sku.getWeight()).setVolume(sku.getVolume()); 60 .setWeight(sku.getWeight()).setVolume(sku.getVolume());
61 // spu 信息 61 // spu 信息
62 orderItem.setSpuName(spu.getName()).setCategoryId(spu.getCategoryId()) 62 orderItem.setSpuName(spu.getName()).setCategoryId(spu.getCategoryId())
63 - .setDeliveryTemplateId(spu.getDeliveryTemplateId()) 63 + .setDeliveryTypes(spu.getDeliveryTypes()).setDeliveryTemplateId(spu.getDeliveryTemplateId())
64 .setGivePoint(spu.getGiveIntegral()).setUsePoint(0); 64 .setGivePoint(spu.getGiveIntegral()).setUsePoint(0);
65 if (StrUtil.isBlank(orderItem.getPicUrl())) { 65 if (StrUtil.isBlank(orderItem.getPicUrl())) {
66 orderItem.setPicUrl(spu.getPicUrl()); 66 orderItem.setPicUrl(spu.getPicUrl());
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; @@ -3,23 +3,30 @@ package cn.iocoder.yudao.module.trade.service.price.calculator;
3 import cn.hutool.core.collection.CollUtil; 3 import cn.hutool.core.collection.CollUtil;
4 import cn.hutool.core.util.ObjectUtil; 4 import cn.hutool.core.util.ObjectUtil;
5 import cn.hutool.core.util.StrUtil; 5 import cn.hutool.core.util.StrUtil;
  6 +import cn.iocoder.yudao.framework.common.util.number.MoneyUtils;
6 import cn.iocoder.yudao.module.promotion.api.reward.RewardActivityApi; 7 import cn.iocoder.yudao.module.promotion.api.reward.RewardActivityApi;
7 import cn.iocoder.yudao.module.promotion.api.reward.dto.RewardActivityMatchRespDTO; 8 import cn.iocoder.yudao.module.promotion.api.reward.dto.RewardActivityMatchRespDTO;
8 import cn.iocoder.yudao.module.promotion.enums.common.PromotionConditionTypeEnum; 9 import cn.iocoder.yudao.module.promotion.enums.common.PromotionConditionTypeEnum;
  10 +import cn.iocoder.yudao.module.promotion.enums.common.PromotionProductScopeEnum;
9 import cn.iocoder.yudao.module.promotion.enums.common.PromotionTypeEnum; 11 import cn.iocoder.yudao.module.promotion.enums.common.PromotionTypeEnum;
10 import cn.iocoder.yudao.module.trade.enums.order.TradeOrderTypeEnum; 12 import cn.iocoder.yudao.module.trade.enums.order.TradeOrderTypeEnum;
11 import cn.iocoder.yudao.module.trade.service.price.bo.TradePriceCalculateReqBO; 13 import cn.iocoder.yudao.module.trade.service.price.bo.TradePriceCalculateReqBO;
12 import cn.iocoder.yudao.module.trade.service.price.bo.TradePriceCalculateRespBO; 14 import cn.iocoder.yudao.module.trade.service.price.bo.TradePriceCalculateRespBO;
  15 +import jakarta.annotation.Resource;
13 import org.springframework.core.annotation.Order; 16 import org.springframework.core.annotation.Order;
14 import org.springframework.stereotype.Component; 17 import org.springframework.stereotype.Component;
15 18
16 -import jakarta.annotation.Resource; 19 +import java.util.ArrayList;
  20 +import java.util.Comparator;
17 import java.util.List; 21 import java.util.List;
  22 +import java.util.Map;
18 23
19 import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertSet; 24 import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertSet;
20 import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.filterList; 25 import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.filterList;
21 import static cn.iocoder.yudao.module.trade.service.price.calculator.TradePriceCalculatorHelper.formatPrice; 26 import static cn.iocoder.yudao.module.trade.service.price.calculator.TradePriceCalculatorHelper.formatPrice;
22 27
  28 +// TODO @puhui999:相关的单测,建议改一改
  29 +
23 /** 30 /**
24 * 满减送活动的 {@link TradePriceCalculator} 实现类 31 * 满减送活动的 {@link TradePriceCalculator} 实现类
25 * 32 *
@@ -52,7 +59,7 @@ public class TradeRewardActivityPriceCalculator implements TradePriceCalculator @@ -52,7 +59,7 @@ public class TradeRewardActivityPriceCalculator implements TradePriceCalculator
52 private void calculate(TradePriceCalculateReqBO param, TradePriceCalculateRespBO result, 59 private void calculate(TradePriceCalculateReqBO param, TradePriceCalculateRespBO result,
53 RewardActivityMatchRespDTO rewardActivity) { 60 RewardActivityMatchRespDTO rewardActivity) {
54 // 1.1 获得满减送的订单项(商品)列表 61 // 1.1 获得满减送的订单项(商品)列表
55 - List<TradePriceCalculateRespBO.OrderItem> orderItems = filterMatchCouponOrderItems(result, rewardActivity); 62 + List<TradePriceCalculateRespBO.OrderItem> orderItems = filterMatchActivityOrderItems(result, rewardActivity);
56 if (CollUtil.isEmpty(orderItems)) { 63 if (CollUtil.isEmpty(orderItems)) {
57 return; 64 return;
58 } 65 }
@@ -61,7 +68,7 @@ public class TradeRewardActivityPriceCalculator implements TradePriceCalculator @@ -61,7 +68,7 @@ public class TradeRewardActivityPriceCalculator implements TradePriceCalculator
61 if (rule == null) { 68 if (rule == null) {
62 TradePriceCalculatorHelper.addNotMatchPromotion(result, orderItems, 69 TradePriceCalculatorHelper.addNotMatchPromotion(result, orderItems,
63 rewardActivity.getId(), rewardActivity.getName(), PromotionTypeEnum.REWARD_ACTIVITY.getType(), 70 rewardActivity.getId(), rewardActivity.getName(), PromotionTypeEnum.REWARD_ACTIVITY.getType(),
64 - getRewardActivityNotMeetTip(rewardActivity)); 71 + getRewardActivityNotMeetTip(rewardActivity, orderItems));
65 return; 72 return;
66 } 73 }
67 74
@@ -84,6 +91,36 @@ public class TradeRewardActivityPriceCalculator implements TradePriceCalculator @@ -84,6 +91,36 @@ public class TradeRewardActivityPriceCalculator implements TradePriceCalculator
84 TradePriceCalculatorHelper.recountPayPrice(orderItem); 91 TradePriceCalculatorHelper.recountPayPrice(orderItem);
85 } 92 }
86 TradePriceCalculatorHelper.recountAllPrice(result); 93 TradePriceCalculatorHelper.recountAllPrice(result);
  94 +
  95 + // 4.1 记录赠送的积分
  96 + if (rule.getPoint() != null && rule.getPoint() > 0) {
  97 + List<Integer> dividePoints = TradePriceCalculatorHelper.dividePrice(orderItems, rule.getPoint());
  98 + for (int i = 0; i < orderItems.size(); i++) {
  99 + // 商品可能赠送了积分,所以这里要加上
  100 + TradePriceCalculateRespBO.OrderItem orderItem = orderItems.get(i);
  101 + orderItem.setGivePoint(orderItem.getGivePoint() + dividePoints.get(i));
  102 + }
  103 + }
  104 + // 4.2 记录订单是否包邮
  105 + if (Boolean.TRUE.equals(rule.getFreeDelivery())) {
  106 + // 只要满足一个活动包邮那么这单就包邮
  107 + result.setFreeDelivery(true);
  108 + }
  109 + // 4.3 记录赠送的优惠券
  110 + if (CollUtil.isNotEmpty(rule.getGiveCouponTemplateCounts())) {
  111 + for (Map.Entry<Long, Integer> entry : rule.getGiveCouponTemplateCounts().entrySet()) {
  112 + Map<Long, Integer> giveCouponTemplateCounts = result.getGiveCouponTemplateCounts();
  113 + // TODO @puhui999:是不是有一种可能性,这个 key 没有,别的 key 有哈。
  114 + // TODO 这里还有一种简化的写法。就是下面,大概两行就可以啦
  115 +// result.getGiveCouponTemplateCounts().put(entry.getKey(),
  116 +// result.getGiveCouponTemplateCounts().getOrDefault(entry.getKey(), 0) + entry.getValue());
  117 + if (giveCouponTemplateCounts.get(entry.getKey()) == null) { // 情况一:还没有赠送的优惠券
  118 + result.setGiveCouponTemplateCounts(rule.getGiveCouponTemplateCounts());
  119 + } else { // 情况二:别的满减活动送过同类优惠券,则直接增加数量
  120 + giveCouponTemplateCounts.put(entry.getKey(), giveCouponTemplateCounts.get(entry.getKey()) + entry.getValue());
  121 + }
  122 + }
  123 + }
87 } 124 }
88 125
89 /** 126 /**
@@ -93,10 +130,23 @@ public class TradeRewardActivityPriceCalculator implements TradePriceCalculator @@ -93,10 +130,23 @@ public class TradeRewardActivityPriceCalculator implements TradePriceCalculator
93 * @param rewardActivity 满减送活动 130 * @param rewardActivity 满减送活动
94 * @return 订单项(商品)列表 131 * @return 订单项(商品)列表
95 */ 132 */
96 - private List<TradePriceCalculateRespBO.OrderItem> filterMatchCouponOrderItems(TradePriceCalculateRespBO result,  
97 - RewardActivityMatchRespDTO rewardActivity) {  
98 - return filterList(result.getItems(),  
99 - orderItem -> CollUtil.contains(rewardActivity.getSpuIds(), orderItem.getSpuId())); 133 + private List<TradePriceCalculateRespBO.OrderItem> filterMatchActivityOrderItems(TradePriceCalculateRespBO result,
  134 + RewardActivityMatchRespDTO rewardActivity) {
  135 + // 情况一:全部商品都可以参与
  136 + if (PromotionProductScopeEnum.isAll(rewardActivity.getProductScope())) {
  137 + return result.getItems();
  138 + }
  139 + // 情况二:指定商品参与
  140 + if (PromotionProductScopeEnum.isSpu(rewardActivity.getProductScope())) {
  141 + return filterList(result.getItems(),
  142 + orderItem -> CollUtil.contains(rewardActivity.getProductScopeValues(), orderItem.getSpuId()));
  143 + }
  144 + // 情况三:指定商品类型参与
  145 + if (PromotionProductScopeEnum.isCategory(rewardActivity.getProductScope())) {
  146 + return filterList(result.getItems(),
  147 + orderItem -> CollUtil.contains(rewardActivity.getProductScopeValues(), orderItem.getCategoryId()));
  148 + }
  149 + return List.of();
100 } 150 }
101 151
102 /** 152 /**
@@ -129,14 +179,30 @@ public class TradeRewardActivityPriceCalculator implements TradePriceCalculator @@ -129,14 +179,30 @@ public class TradeRewardActivityPriceCalculator implements TradePriceCalculator
129 } 179 }
130 180
131 /** 181 /**
132 - * 获得满减送活动部匹配时的提示 182 + * 获得满减送活动不匹配时的提示
133 * 183 *
134 * @param rewardActivity 满减送活动 184 * @param rewardActivity 满减送活动
135 * @return 提示 185 * @return 提示
136 */ 186 */
137 - private String getRewardActivityNotMeetTip(RewardActivityMatchRespDTO rewardActivity) {  
138 - // TODO 芋艿:后面再想想;应该找第一个规则,算下还差多少即可。  
139 - return "TODO"; 187 + private String getRewardActivityNotMeetTip(RewardActivityMatchRespDTO rewardActivity,
  188 + List<TradePriceCalculateRespBO.OrderItem> orderItems) {
  189 + // 1. 计算数量和价格
  190 + Integer count = TradePriceCalculatorHelper.calculateTotalCount(orderItems);
  191 + Integer price = TradePriceCalculatorHelper.calculateTotalPayPrice(orderItems);
  192 + assert count != null && price != null;
  193 +
  194 + // 2. 构建不满足时的提示信息:按最低档规则算
  195 + String meetTip = "满减送:购满 {} {},可以减 {} 元";
  196 + List<RewardActivityMatchRespDTO.Rule> rules = new ArrayList<>(rewardActivity.getRules());
  197 + rules.sort(Comparator.comparing(RewardActivityMatchRespDTO.Rule::getLimit)); // 按优惠门槛升序
  198 + RewardActivityMatchRespDTO.Rule rule = rules.get(0);
  199 + if (PromotionConditionTypeEnum.PRICE.getType().equals(rewardActivity.getConditionType())) {
  200 + return StrUtil.format(meetTip, rule.getLimit(), "元", MoneyUtils.fenToYuanStr(rule.getDiscountPrice()));
  201 + }
  202 + if (PromotionConditionTypeEnum.COUNT.getType().equals(rewardActivity.getConditionType())) {
  203 + return StrUtil.format(meetTip, rule.getLimit(), "件", MoneyUtils.fenToYuanStr(rule.getDiscountPrice()));
  204 + }
  205 + return StrUtil.EMPTY;
140 } 206 }
141 207
142 } 208 }
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; @@ -4,6 +4,7 @@ import cn.iocoder.yudao.framework.test.core.ut.BaseMockitoUnitTest;
4 import cn.iocoder.yudao.module.promotion.api.reward.RewardActivityApi; 4 import cn.iocoder.yudao.module.promotion.api.reward.RewardActivityApi;
5 import cn.iocoder.yudao.module.promotion.api.reward.dto.RewardActivityMatchRespDTO; 5 import cn.iocoder.yudao.module.promotion.api.reward.dto.RewardActivityMatchRespDTO;
6 import cn.iocoder.yudao.module.promotion.enums.common.PromotionConditionTypeEnum; 6 import cn.iocoder.yudao.module.promotion.enums.common.PromotionConditionTypeEnum;
  7 +import cn.iocoder.yudao.module.promotion.enums.common.PromotionProductScopeEnum;
7 import cn.iocoder.yudao.module.promotion.enums.common.PromotionTypeEnum; 8 import cn.iocoder.yudao.module.promotion.enums.common.PromotionTypeEnum;
8 import cn.iocoder.yudao.module.trade.enums.order.TradeOrderTypeEnum; 9 import cn.iocoder.yudao.module.trade.enums.order.TradeOrderTypeEnum;
9 import cn.iocoder.yudao.module.trade.service.price.bo.TradePriceCalculateReqBO; 10 import cn.iocoder.yudao.module.trade.service.price.bo.TradePriceCalculateReqBO;
@@ -13,6 +14,7 @@ import org.mockito.InjectMocks; @@ -13,6 +14,7 @@ import org.mockito.InjectMocks;
13 import org.mockito.Mock; 14 import org.mockito.Mock;
14 15
15 import java.util.ArrayList; 16 import java.util.ArrayList;
  17 +import java.util.LinkedHashMap;
16 18
17 import static cn.iocoder.yudao.framework.common.util.collection.SetUtils.asSet; 19 import static cn.iocoder.yudao.framework.common.util.collection.SetUtils.asSet;
18 import static cn.iocoder.yudao.framework.test.core.util.RandomUtils.randomPojo; 20 import static cn.iocoder.yudao.framework.test.core.util.RandomUtils.randomPojo;
@@ -47,7 +49,7 @@ public class TradeRewardActivityPriceCalculatorTest extends BaseMockitoUnitTest @@ -47,7 +49,7 @@ public class TradeRewardActivityPriceCalculatorTest extends BaseMockitoUnitTest
47 TradePriceCalculateRespBO result = new TradePriceCalculateRespBO() 49 TradePriceCalculateRespBO result = new TradePriceCalculateRespBO()
48 .setType(TradeOrderTypeEnum.NORMAL.getType()) 50 .setType(TradeOrderTypeEnum.NORMAL.getType())
49 .setPrice(new TradePriceCalculateRespBO.Price()) 51 .setPrice(new TradePriceCalculateRespBO.Price())
50 - .setPromotions(new ArrayList<>()) 52 + .setPromotions(new ArrayList<>()).setGiveCouponTemplateCounts(new LinkedHashMap<>())
51 .setItems(asList( 53 .setItems(asList(
52 new TradePriceCalculateRespBO.OrderItem().setSkuId(10L).setCount(2).setSelected(true) 54 new TradePriceCalculateRespBO.OrderItem().setSkuId(10L).setCount(2).setSelected(true)
53 .setPrice(100).setSpuId(1L), 55 .setPrice(100).setSpuId(1L),
@@ -60,16 +62,22 @@ public class TradeRewardActivityPriceCalculatorTest extends BaseMockitoUnitTest @@ -60,16 +62,22 @@ public class TradeRewardActivityPriceCalculatorTest extends BaseMockitoUnitTest
60 TradePriceCalculatorHelper.recountPayPrice(result.getItems()); 62 TradePriceCalculatorHelper.recountPayPrice(result.getItems());
61 TradePriceCalculatorHelper.recountAllPrice(result); 63 TradePriceCalculatorHelper.recountAllPrice(result);
62 64
63 - // mock 方法(限时折扣 DiscountActivity 信息) 65 + // mock 方法(满减送 RewardActivity 信息)
64 when(rewardActivityApi.getMatchRewardActivityList(eq(asSet(1L, 2L, 3L)))).thenReturn(asList( 66 when(rewardActivityApi.getMatchRewardActivityList(eq(asSet(1L, 2L, 3L)))).thenReturn(asList(
65 randomPojo(RewardActivityMatchRespDTO.class, o -> o.setId(1000L).setName("活动 1000 号") 67 randomPojo(RewardActivityMatchRespDTO.class, o -> o.setId(1000L).setName("活动 1000 号")
66 - .setSpuIds(asList(1L, 2L)).setConditionType(PromotionConditionTypeEnum.PRICE.getType())  
67 - .setRules(singletonList(new RewardActivityMatchRespDTO.Rule().setLimit(200).setDiscountPrice(70)))), 68 + .setConditionType(PromotionConditionTypeEnum.PRICE.getType())
  69 + .setProductScope(PromotionProductScopeEnum.SPU.getScope()).setProductScopeValues(asList(1L, 2L))
  70 + .setRules(singletonList(new RewardActivityMatchRespDTO.Rule().setLimit(20).setDiscountPrice(70)
  71 + .setFreeDelivery(false)))),
68 randomPojo(RewardActivityMatchRespDTO.class, o -> o.setId(2000L).setName("活动 2000 号") 72 randomPojo(RewardActivityMatchRespDTO.class, o -> o.setId(2000L).setName("活动 2000 号")
69 - .setSpuIds(singletonList(3L)).setConditionType(PromotionConditionTypeEnum.COUNT.getType())  
70 - .setRules(asList(new RewardActivityMatchRespDTO.Rule().setLimit(1).setDiscountPrice(10),  
71 - new RewardActivityMatchRespDTO.Rule().setLimit(2).setDiscountPrice(60), // 最大可满足,因为是 4 个  
72 - new RewardActivityMatchRespDTO.Rule().setLimit(10).setDiscountPrice(100)))) 73 + .setConditionType(PromotionConditionTypeEnum.COUNT.getType())
  74 + .setProductScope(PromotionProductScopeEnum.SPU.getScope()).setProductScopeValues(singletonList(3L))
  75 + .setRules(asList(new RewardActivityMatchRespDTO.Rule().setLimit(1).setDiscountPrice(10)
  76 + .setPoint(50).setFreeDelivery(false),
  77 + new RewardActivityMatchRespDTO.Rule().setLimit(2).setDiscountPrice(60)
  78 + .setPoint(100).setFreeDelivery(false), // 最大可满足,因为是 4 个
  79 + new RewardActivityMatchRespDTO.Rule().setLimit(10).setDiscountPrice(100)
  80 + .setFreeDelivery(false))))
73 )); 81 ));
74 82
75 // 调用 83 // 调用
@@ -94,6 +102,7 @@ public class TradeRewardActivityPriceCalculatorTest extends BaseMockitoUnitTest @@ -94,6 +102,7 @@ public class TradeRewardActivityPriceCalculatorTest extends BaseMockitoUnitTest
94 assertEquals(orderItem01.getCouponPrice(), 0); 102 assertEquals(orderItem01.getCouponPrice(), 0);
95 assertEquals(orderItem01.getPointPrice(), 0); 103 assertEquals(orderItem01.getPointPrice(), 0);
96 assertEquals(orderItem01.getPayPrice(), 160); 104 assertEquals(orderItem01.getPayPrice(), 160);
  105 + assertEquals(orderItem01.getGivePoint(), 0);
97 // 断言:SKU 2 106 // 断言:SKU 2
98 TradePriceCalculateRespBO.OrderItem orderItem02 = result.getItems().get(1); 107 TradePriceCalculateRespBO.OrderItem orderItem02 = result.getItems().get(1);
99 assertEquals(orderItem02.getSkuId(), 20L); 108 assertEquals(orderItem02.getSkuId(), 20L);
@@ -104,6 +113,7 @@ public class TradeRewardActivityPriceCalculatorTest extends BaseMockitoUnitTest @@ -104,6 +113,7 @@ public class TradeRewardActivityPriceCalculatorTest extends BaseMockitoUnitTest
104 assertEquals(orderItem02.getCouponPrice(), 0); 113 assertEquals(orderItem02.getCouponPrice(), 0);
105 assertEquals(orderItem02.getPointPrice(), 0); 114 assertEquals(orderItem02.getPointPrice(), 0);
106 assertEquals(orderItem02.getPayPrice(), 120); 115 assertEquals(orderItem02.getPayPrice(), 120);
  116 + assertEquals(orderItem02.getGivePoint(), 0);
107 // 断言:SKU 3 117 // 断言:SKU 3
108 TradePriceCalculateRespBO.OrderItem orderItem03 = result.getItems().get(2); 118 TradePriceCalculateRespBO.OrderItem orderItem03 = result.getItems().get(2);
109 assertEquals(orderItem03.getSkuId(), 30L); 119 assertEquals(orderItem03.getSkuId(), 30L);
@@ -114,6 +124,7 @@ public class TradeRewardActivityPriceCalculatorTest extends BaseMockitoUnitTest @@ -114,6 +124,7 @@ public class TradeRewardActivityPriceCalculatorTest extends BaseMockitoUnitTest
114 assertEquals(orderItem03.getCouponPrice(), 0); 124 assertEquals(orderItem03.getCouponPrice(), 0);
115 assertEquals(orderItem03.getPointPrice(), 0); 125 assertEquals(orderItem03.getPointPrice(), 0);
116 assertEquals(orderItem03.getPayPrice(), 60); 126 assertEquals(orderItem03.getPayPrice(), 60);
  127 + assertEquals(orderItem03.getGivePoint(), 100);
117 // 断言:Promotion 部分(第一个) 128 // 断言:Promotion 部分(第一个)
118 assertEquals(result.getPromotions().size(), 2); 129 assertEquals(result.getPromotions().size(), 2);
119 TradePriceCalculateRespBO.Promotion promotion01 = result.getPromotions().get(0); 130 TradePriceCalculateRespBO.Promotion promotion01 = result.getPromotions().get(0);
@@ -175,7 +186,7 @@ public class TradeRewardActivityPriceCalculatorTest extends BaseMockitoUnitTest @@ -175,7 +186,7 @@ public class TradeRewardActivityPriceCalculatorTest extends BaseMockitoUnitTest
175 // mock 方法(限时折扣 DiscountActivity 信息) 186 // mock 方法(限时折扣 DiscountActivity 信息)
176 when(rewardActivityApi.getMatchRewardActivityList(eq(asSet(1L, 2L)))).thenReturn(singletonList( 187 when(rewardActivityApi.getMatchRewardActivityList(eq(asSet(1L, 2L)))).thenReturn(singletonList(
177 randomPojo(RewardActivityMatchRespDTO.class, o -> o.setId(1000L).setName("活动 1000 号") 188 randomPojo(RewardActivityMatchRespDTO.class, o -> o.setId(1000L).setName("活动 1000 号")
178 - .setSpuIds(asList(1L, 2L)).setConditionType(PromotionConditionTypeEnum.PRICE.getType()) 189 + .setProductScopeValues(asList(1L, 2L)).setConditionType(PromotionConditionTypeEnum.PRICE.getType())
179 .setRules(singletonList(new RewardActivityMatchRespDTO.Rule().setLimit(351).setDiscountPrice(70)))) 190 .setRules(singletonList(new RewardActivityMatchRespDTO.Rule().setLimit(351).setDiscountPrice(70))))
180 )); 191 ));
181 192
yudao-module-mall/yudao-module-trade-biz/src/test/resources/sql/create_tables.sql
@@ -48,6 +48,7 @@ CREATE TABLE IF NOT EXISTS &quot;trade_order&quot; @@ -48,6 +48,7 @@ CREATE TABLE IF NOT EXISTS &quot;trade_order&quot;
48 "give_point" int NULL, 48 "give_point" int NULL,
49 "refund_point" int NULL, 49 "refund_point" int NULL,
50 "vip_price" int NULL, 50 "vip_price" int NULL,
  51 + "give_coupons_map" varchar NULL,
51 "seckill_activity_id" long NULL, 52 "seckill_activity_id" long NULL,
52 "bargain_activity_id" long NULL, 53 "bargain_activity_id" long NULL,
53 "bargain_record_id" long NULL, 54 "bargain_record_id" long NULL,
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; @@ -18,12 +18,12 @@ import cn.iocoder.yudao.module.member.service.user.MemberUserService;
18 import io.swagger.v3.oas.annotations.Operation; 18 import io.swagger.v3.oas.annotations.Operation;
19 import io.swagger.v3.oas.annotations.Parameter; 19 import io.swagger.v3.oas.annotations.Parameter;
20 import io.swagger.v3.oas.annotations.tags.Tag; 20 import io.swagger.v3.oas.annotations.tags.Tag;
  21 +import jakarta.annotation.Resource;
  22 +import jakarta.validation.Valid;
21 import org.springframework.security.access.prepost.PreAuthorize; 23 import org.springframework.security.access.prepost.PreAuthorize;
22 import org.springframework.validation.annotation.Validated; 24 import org.springframework.validation.annotation.Validated;
23 import org.springframework.web.bind.annotation.*; 25 import org.springframework.web.bind.annotation.*;
24 26
25 -import jakarta.annotation.Resource;  
26 -import jakarta.validation.Valid;  
27 import java.util.Collection; 27 import java.util.Collection;
28 import java.util.List; 28 import java.util.List;
29 import java.util.Objects; 29 import java.util.Objects;
@@ -76,14 +76,6 @@ public class MemberUserController { @@ -76,14 +76,6 @@ public class MemberUserController {
76 return success(true); 76 return success(true);
77 } 77 }
78 78
79 - @PutMapping("/update-balance")  
80 - @Operation(summary = "更新会员用户余额")  
81 - @PreAuthorize("@ss.hasPermission('member:user:update-balance')")  
82 - public CommonResult<Boolean> updateUserBalance(@Valid @RequestBody Long id) {  
83 - // todo @jason:增加一个【修改余额】  
84 - return success(true);  
85 - }  
86 -  
87 @GetMapping("/get") 79 @GetMapping("/get")
88 @Operation(summary = "获得会员用户") 80 @Operation(summary = "获得会员用户")
89 @Parameter(name = "id", description = "编号", required = true, example = "1024") 81 @Parameter(name = "id", description = "编号", required = true, example = "1024")
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 { @@ -18,9 +18,8 @@ public enum PayWalletBizTypeEnum implements IntArrayValuable {
18 RECHARGE(1, "充值"), 18 RECHARGE(1, "充值"),
19 RECHARGE_REFUND(2, "充值退款"), 19 RECHARGE_REFUND(2, "充值退款"),
20 PAYMENT(3, "支付"), 20 PAYMENT(3, "支付"),
21 - PAYMENT_REFUND(4, "支付退款");  
22 -  
23 - // TODO 后续增加 21 + PAYMENT_REFUND(4, "支付退款"),
  22 + UPDATE_BALANCE(5, "更新余额");
24 23
25 /** 24 /**
26 * 业务分类 25 * 业务分类
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; @@ -4,9 +4,11 @@ import cn.iocoder.yudao.framework.common.pojo.CommonResult;
4 import cn.iocoder.yudao.framework.common.pojo.PageResult; 4 import cn.iocoder.yudao.framework.common.pojo.PageResult;
5 import cn.iocoder.yudao.module.pay.controller.admin.wallet.vo.wallet.PayWalletPageReqVO; 5 import cn.iocoder.yudao.module.pay.controller.admin.wallet.vo.wallet.PayWalletPageReqVO;
6 import cn.iocoder.yudao.module.pay.controller.admin.wallet.vo.wallet.PayWalletRespVO; 6 import cn.iocoder.yudao.module.pay.controller.admin.wallet.vo.wallet.PayWalletRespVO;
  7 +import cn.iocoder.yudao.module.pay.controller.admin.wallet.vo.wallet.PayWalletUpdateBalanceReqVO;
7 import cn.iocoder.yudao.module.pay.controller.admin.wallet.vo.wallet.PayWalletUserReqVO; 8 import cn.iocoder.yudao.module.pay.controller.admin.wallet.vo.wallet.PayWalletUserReqVO;
8 import cn.iocoder.yudao.module.pay.convert.wallet.PayWalletConvert; 9 import cn.iocoder.yudao.module.pay.convert.wallet.PayWalletConvert;
9 import cn.iocoder.yudao.module.pay.dal.dataobject.wallet.PayWalletDO; 10 import cn.iocoder.yudao.module.pay.dal.dataobject.wallet.PayWalletDO;
  11 +import cn.iocoder.yudao.module.pay.enums.wallet.PayWalletBizTypeEnum;
10 import cn.iocoder.yudao.module.pay.service.wallet.PayWalletService; 12 import cn.iocoder.yudao.module.pay.service.wallet.PayWalletService;
11 import io.swagger.v3.oas.annotations.Operation; 13 import io.swagger.v3.oas.annotations.Operation;
12 import io.swagger.v3.oas.annotations.tags.Tag; 14 import io.swagger.v3.oas.annotations.tags.Tag;
@@ -15,12 +17,12 @@ import jakarta.validation.Valid; @@ -15,12 +17,12 @@ import jakarta.validation.Valid;
15 import lombok.extern.slf4j.Slf4j; 17 import lombok.extern.slf4j.Slf4j;
16 import org.springframework.security.access.prepost.PreAuthorize; 18 import org.springframework.security.access.prepost.PreAuthorize;
17 import org.springframework.validation.annotation.Validated; 19 import org.springframework.validation.annotation.Validated;
18 -import org.springframework.web.bind.annotation.GetMapping;  
19 -import org.springframework.web.bind.annotation.RequestMapping;  
20 -import org.springframework.web.bind.annotation.RestController; 20 +import org.springframework.web.bind.annotation.*;
21 21
22 import static cn.iocoder.yudao.framework.common.enums.UserTypeEnum.MEMBER; 22 import static cn.iocoder.yudao.framework.common.enums.UserTypeEnum.MEMBER;
  23 +import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception;
23 import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success; 24 import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success;
  25 +import static cn.iocoder.yudao.module.pay.enums.ErrorCodeConstants.WALLET_NOT_FOUND;
24 26
25 @Tag(name = "管理后台 - 用户钱包") 27 @Tag(name = "管理后台 - 用户钱包")
26 @RestController 28 @RestController
@@ -48,4 +50,21 @@ public class PayWalletController { @@ -48,4 +50,21 @@ public class PayWalletController {
48 return success(PayWalletConvert.INSTANCE.convertPage(pageResult)); 50 return success(PayWalletConvert.INSTANCE.convertPage(pageResult));
49 } 51 }
50 52
  53 + @PutMapping("/update-balance")
  54 + @Operation(summary = "更新会员用户余额")
  55 + @PreAuthorize("@ss.hasPermission('pay:wallet:update-balance')")
  56 + public CommonResult<Boolean> updateWalletBalance(@Valid @RequestBody PayWalletUpdateBalanceReqVO updateReqVO) {
  57 + // 获得用户钱包
  58 + PayWalletDO wallet = payWalletService.getOrCreateWallet(updateReqVO.getUserId(), MEMBER.getValue());
  59 + if (wallet == null) {
  60 + log.error("[updateWalletBalance],updateReqVO({}) 用户钱包不存在.", updateReqVO);
  61 + throw exception(WALLET_NOT_FOUND);
  62 + }
  63 +
  64 + // 更新钱包余额
  65 + payWalletService.addWalletBalance(wallet.getId(), String.valueOf(updateReqVO.getUserId()),
  66 + PayWalletBizTypeEnum.UPDATE_BALANCE, updateReqVO.getBalance());
  67 + return success(true);
  68 + }
  69 +
51 } 70 }
yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/controller/admin/wallet/vo/wallet/PayWalletUpdateBalanceReqVO.java 0 → 100644
  1 +package cn.iocoder.yudao.module.pay.controller.admin.wallet.vo.wallet;
  2 +
  3 +import io.swagger.v3.oas.annotations.media.Schema;
  4 +import jakarta.validation.constraints.NotNull;
  5 +import lombok.Data;
  6 +
  7 +@Schema(description = "管理后台 - 修改钱包余额 Request VO")
  8 +@Data
  9 +public class PayWalletUpdateBalanceReqVO {
  10 +
  11 + @Schema(description = "用户编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "23788")
  12 + @NotNull(message = "用户编号不能为空")
  13 + private Long userId;
  14 +
  15 + @Schema(description = "变动余额,正数为增加,负数为减少", requiredMode = Schema.RequiredMode.REQUIRED, example = "100")
  16 + @NotNull(message = "变动余额不能为空")
  17 + private Integer balance;
  18 +
  19 +}
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; @@ -12,12 +12,12 @@ import cn.iocoder.yudao.module.pay.enums.wallet.PayWalletBizTypeEnum;
12 import cn.iocoder.yudao.module.pay.service.order.PayOrderService; 12 import cn.iocoder.yudao.module.pay.service.order.PayOrderService;
13 import cn.iocoder.yudao.module.pay.service.refund.PayRefundService; 13 import cn.iocoder.yudao.module.pay.service.refund.PayRefundService;
14 import cn.iocoder.yudao.module.pay.service.wallet.bo.WalletTransactionCreateReqBO; 14 import cn.iocoder.yudao.module.pay.service.wallet.bo.WalletTransactionCreateReqBO;
  15 +import jakarta.annotation.Resource;
15 import lombok.extern.slf4j.Slf4j; 16 import lombok.extern.slf4j.Slf4j;
16 import org.springframework.context.annotation.Lazy; 17 import org.springframework.context.annotation.Lazy;
17 import org.springframework.stereotype.Service; 18 import org.springframework.stereotype.Service;
18 import org.springframework.transaction.annotation.Transactional; 19 import org.springframework.transaction.annotation.Transactional;
19 20
20 -import jakarta.annotation.Resource;  
21 import java.time.LocalDateTime; 21 import java.time.LocalDateTime;
22 22
23 import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception; 23 import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception;
@@ -176,6 +176,9 @@ public class PayWalletServiceImpl implements PayWalletService { @@ -176,6 +176,9 @@ public class PayWalletServiceImpl implements PayWalletService {
176 walletMapper.updateWhenRecharge(payWallet.getId(), price); 176 walletMapper.updateWhenRecharge(payWallet.getId(), price);
177 break; 177 break;
178 } 178 }
  179 + case UPDATE_BALANCE: // 更新余额
  180 + walletMapper.updateWhenRecharge(payWallet.getId(), price);
  181 + break;
179 default: { 182 default: {
180 // TODO 其它类型待实现 183 // TODO 其它类型待实现
181 throw new UnsupportedOperationException("待实现"); 184 throw new UnsupportedOperationException("待实现");
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; @@ -6,9 +6,7 @@ import cn.iocoder.yudao.module.system.framework.sms.core.enums.SmsChannelEnum;
6 import cn.iocoder.yudao.module.system.service.sms.SmsSendService; 6 import cn.iocoder.yudao.module.system.service.sms.SmsSendService;
7 import io.swagger.v3.oas.annotations.Operation; 7 import io.swagger.v3.oas.annotations.Operation;
8 import io.swagger.v3.oas.annotations.tags.Tag; 8 import io.swagger.v3.oas.annotations.tags.Tag;
9 -import org.springframework.web.bind.annotation.PostMapping;  
10 -import org.springframework.web.bind.annotation.RequestMapping;  
11 -import org.springframework.web.bind.annotation.RestController; 9 +import org.springframework.web.bind.annotation.*;
12 10
13 import jakarta.annotation.Resource; 11 import jakarta.annotation.Resource;
14 import jakarta.annotation.security.PermitAll; 12 import jakarta.annotation.security.PermitAll;
@@ -26,7 +24,7 @@ public class SmsCallbackController { @@ -26,7 +24,7 @@ public class SmsCallbackController {
26 24
27 @PostMapping("/aliyun") 25 @PostMapping("/aliyun")
28 @PermitAll 26 @PermitAll
29 - @Operation(summary = "阿里云短信的回调", description = "参见 https://help.aliyun.com/zh/sms/developer-reference/configure-delivery-receipts-1 文档") 27 + @Operation(summary = "阿里云短信的回调", description = "参见 https://help.aliyun.com/document_detail/120998.html 文档")
30 public CommonResult<Boolean> receiveAliyunSmsStatus(HttpServletRequest request) throws Throwable { 28 public CommonResult<Boolean> receiveAliyunSmsStatus(HttpServletRequest request) throws Throwable {
31 String text = ServletUtils.getBody(request); 29 String text = ServletUtils.getBody(request);
32 smsSendService.receiveSmsStatus(SmsChannelEnum.ALIYUN.getCode(), text); 30 smsSendService.receiveSmsStatus(SmsChannelEnum.ALIYUN.getCode(), text);
@@ -35,7 +33,7 @@ public class SmsCallbackController { @@ -35,7 +33,7 @@ public class SmsCallbackController {
35 33
36 @PostMapping("/tencent") 34 @PostMapping("/tencent")
37 @PermitAll 35 @PermitAll
38 - @Operation(summary = "腾讯云短信的回调", description = "参见 https://cloud.tencent.com/document/product/382/59178 文档") 36 + @Operation(summary = "腾讯云短信的回调", description = "参见 https://cloud.tencent.com/document/product/382/52077 文档")
39 public CommonResult<Boolean> receiveTencentSmsStatus(HttpServletRequest request) throws Throwable { 37 public CommonResult<Boolean> receiveTencentSmsStatus(HttpServletRequest request) throws Throwable {
40 String text = ServletUtils.getBody(request); 38 String text = ServletUtils.getBody(request);
41 smsSendService.receiveSmsStatus(SmsChannelEnum.TENCENT.getCode(), text); 39 smsSendService.receiveSmsStatus(SmsChannelEnum.TENCENT.getCode(), text);
@@ -46,10 +44,17 @@ public class SmsCallbackController { @@ -46,10 +44,17 @@ public class SmsCallbackController {
46 @PostMapping("/huawei") 44 @PostMapping("/huawei")
47 @PermitAll 45 @PermitAll
48 @Operation(summary = "华为云短信的回调", description = "参见 https://support.huaweicloud.com/api-msgsms/sms_05_0003.html 文档") 46 @Operation(summary = "华为云短信的回调", description = "参见 https://support.huaweicloud.com/api-msgsms/sms_05_0003.html 文档")
49 - public CommonResult<Boolean> receiveHuaweiSmsStatus(HttpServletRequest request) throws Throwable {  
50 - String text = ServletUtils.getBody(request);  
51 - smsSendService.receiveSmsStatus(SmsChannelEnum.HUAWEI.getCode(), text); 47 + public CommonResult<Boolean> receiveHuaweiSmsStatus(@RequestBody String requestBody) throws Throwable {
  48 + smsSendService.receiveSmsStatus(SmsChannelEnum.HUAWEI.getCode(), requestBody);
  49 + return success(true);
  50 + }
  51 +
  52 + @PostMapping("/qiniu")
  53 + @PermitAll
  54 + @Operation(summary = "七牛云短信的回调", description = "参见 https://developer.qiniu.com/sms/5910/message-push 文档")
  55 + public CommonResult<Boolean> receiveQiniuSmsStatus(@RequestBody String requestBody) throws Throwable {
  56 + smsSendService.receiveSmsStatus(SmsChannelEnum.QINIU.getCode(), requestBody);
52 return success(true); 57 return success(true);
53 } 58 }
54 59
55 -} 60 -}
  61 +}
56 \ No newline at end of file 62 \ No newline at end of file
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 { @@ -30,7 +30,8 @@ public interface SmsClientFactory {
30 * 创建短信 Client 30 * 创建短信 Client
31 * 31 *
32 * @param properties 配置对象 32 * @param properties 配置对象
  33 + * @return 短信 Client
33 */ 34 */
34 - void createOrUpdateSmsClient(SmsChannelProperties properties); 35 + SmsClient createOrUpdateSmsClient(SmsChannelProperties properties);
35 36
36 } 37 }
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 { @@ -26,15 +26,9 @@ public abstract class AbstractSmsClient implements SmsClient {
26 * 初始化 26 * 初始化
27 */ 27 */
28 public final void init() { 28 public final void init() {
29 - doInit();  
30 log.debug("[init][配置({}) 初始化完成]", properties); 29 log.debug("[init][配置({}) 初始化完成]", properties);
31 } 30 }
32 31
33 - /**  
34 - * 自定义初始化  
35 - */  
36 - protected abstract void doInit();  
37 -  
38 public final void refresh(SmsChannelProperties properties) { 32 public final void refresh(SmsChannelProperties properties) {
39 // 判断是否更新 33 // 判断是否更新
40 if (properties.equals(this.properties)) { 34 if (properties.equals(this.properties)) {
yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/framework/sms/core/client/impl/AliyunSmsClient.java
@@ -51,10 +51,6 @@ public class AliyunSmsClient extends AbstractSmsClient { @@ -51,10 +51,6 @@ public class AliyunSmsClient extends AbstractSmsClient {
51 } 51 }
52 52
53 @Override 53 @Override
54 - protected void doInit() {  
55 - }  
56 -  
57 - @Override  
58 public SmsSendRespDTO sendSms(Long sendLogId, String mobile, String apiTemplateId, 54 public SmsSendRespDTO sendSms(Long sendLogId, String mobile, String apiTemplateId,
59 List<KeyValue<String, Object>> templateParams) throws Throwable { 55 List<KeyValue<String, Object>> templateParams) throws Throwable {
60 Assert.notBlank(properties.getSignature(), "短信签名不能为空"); 56 Assert.notBlank(properties.getSignature(), "短信签名不能为空");
@@ -80,7 +76,7 @@ public class AliyunSmsClient extends AbstractSmsClient { @@ -80,7 +76,7 @@ public class AliyunSmsClient extends AbstractSmsClient {
80 @Override 76 @Override
81 public List<SmsReceiveRespDTO> parseSmsReceiveStatus(String text) { 77 public List<SmsReceiveRespDTO> parseSmsReceiveStatus(String text) {
82 JSONArray statuses = JSONUtil.parseArray(text); 78 JSONArray statuses = JSONUtil.parseArray(text);
83 - // 字段参考 79 + // 字段参考 https://help.aliyun.com/zh/sms/developer-reference/smsreport-2
84 return convertList(statuses, status -> { 80 return convertList(statuses, status -> {
85 JSONObject statusObj = (JSONObject) status; 81 JSONObject statusObj = (JSONObject) status;
86 return new SmsReceiveRespDTO() 82 return new SmsReceiveRespDTO()
@@ -166,7 +162,8 @@ public class AliyunSmsClient extends AbstractSmsClient { @@ -166,7 +162,8 @@ public class AliyunSmsClient extends AbstractSmsClient {
166 String hashedRequestBody = DigestUtil.sha256Hex(requestBody); 162 String hashedRequestBody = DigestUtil.sha256Hex(requestBody);
167 163
168 // 4. 构建 Authorization 签名 164 // 4. 构建 Authorization 签名
169 - String canonicalRequest = "POST" + "\n" + "/" + "\n" + queryString + "\n" + canonicalHeaders + "\n" + signedHeaders + "\n" + hashedRequestBody; 165 + String canonicalRequest = "POST" + "\n" + "/" + "\n" + queryString + "\n"
  166 + + canonicalHeaders + "\n" + signedHeaders + "\n" + hashedRequestBody;
170 String hashedCanonicalRequest = DigestUtil.sha256Hex(canonicalRequest); 167 String hashedCanonicalRequest = DigestUtil.sha256Hex(canonicalRequest);
171 String stringToSign = "ACS3-HMAC-SHA256" + "\n" + hashedCanonicalRequest; 168 String stringToSign = "ACS3-HMAC-SHA256" + "\n" + hashedCanonicalRequest;
172 String signature = SecureUtil.hmacSha256(properties.getApiSecret()).digestHex(stringToSign); // 计算签名 169 String signature = SecureUtil.hmacSha256(properties.getApiSecret()).digestHex(stringToSign); // 计算签名
yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/framework/sms/core/client/impl/DebugDingTalkSmsClient.java
@@ -37,10 +37,6 @@ public class DebugDingTalkSmsClient extends AbstractSmsClient { @@ -37,10 +37,6 @@ public class DebugDingTalkSmsClient extends AbstractSmsClient {
37 } 37 }
38 38
39 @Override 39 @Override
40 - protected void doInit() {  
41 - }  
42 -  
43 - @Override  
44 public SmsSendRespDTO sendSms(Long sendLogId, String mobile, 40 public SmsSendRespDTO sendSms(Long sendLogId, String mobile,
45 String apiTemplateId, List<KeyValue<String, Object>> templateParams) throws Throwable { 41 String apiTemplateId, List<KeyValue<String, Object>> templateParams) throws Throwable {
46 // 构建请求 42 // 构建请求
yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/framework/sms/core/client/impl/HuaweiSmsClient.java
1 package cn.iocoder.yudao.module.system.framework.sms.core.client.impl; 1 package cn.iocoder.yudao.module.system.framework.sms.core.client.impl;
2 2
  3 +import cn.hutool.core.collection.ListUtil;
  4 +import cn.hutool.core.date.format.FastDateFormat;
3 import cn.hutool.core.lang.Assert; 5 import cn.hutool.core.lang.Assert;
  6 +import cn.hutool.core.util.CharsetUtil;
4 import cn.hutool.core.util.StrUtil; 7 import cn.hutool.core.util.StrUtil;
5 -  
6 import cn.hutool.crypto.SecureUtil; 8 import cn.hutool.crypto.SecureUtil;
7 -import cn.hutool.http.HttpRequest;  
8 -import cn.hutool.http.HttpResponse; 9 +import cn.hutool.http.HttpUtil;
9 import cn.hutool.json.JSONObject; 10 import cn.hutool.json.JSONObject;
10 import cn.hutool.json.JSONUtil; 11 import cn.hutool.json.JSONUtil;
11 import cn.iocoder.yudao.framework.common.core.KeyValue; 12 import cn.iocoder.yudao.framework.common.core.KeyValue;
12 -import cn.iocoder.yudao.framework.common.util.collection.CollectionUtils; 13 +import cn.iocoder.yudao.framework.common.util.http.HttpUtils;
13 import cn.iocoder.yudao.framework.common.util.json.JsonUtils; 14 import cn.iocoder.yudao.framework.common.util.json.JsonUtils;
14 import cn.iocoder.yudao.module.system.framework.sms.core.client.dto.SmsReceiveRespDTO; 15 import cn.iocoder.yudao.module.system.framework.sms.core.client.dto.SmsReceiveRespDTO;
15 import cn.iocoder.yudao.module.system.framework.sms.core.client.dto.SmsSendRespDTO; 16 import cn.iocoder.yudao.module.system.framework.sms.core.client.dto.SmsSendRespDTO;
16 import cn.iocoder.yudao.module.system.framework.sms.core.client.dto.SmsTemplateRespDTO; 17 import cn.iocoder.yudao.module.system.framework.sms.core.client.dto.SmsTemplateRespDTO;
17 import cn.iocoder.yudao.module.system.framework.sms.core.enums.SmsTemplateAuditStatusEnum; 18 import cn.iocoder.yudao.module.system.framework.sms.core.enums.SmsTemplateAuditStatusEnum;
18 import cn.iocoder.yudao.module.system.framework.sms.core.property.SmsChannelProperties; 19 import cn.iocoder.yudao.module.system.framework.sms.core.property.SmsChannelProperties;
19 -  
20 -import com.fasterxml.jackson.annotation.JsonFormat;  
21 -import com.fasterxml.jackson.annotation.JsonProperty;  
22 -import lombok.Data;  
23 import lombok.extern.slf4j.Slf4j; 20 import lombok.extern.slf4j.Slf4j;
24 21
25 import java.io.UnsupportedEncodingException; 22 import java.io.UnsupportedEncodingException;
26 import java.net.URLEncoder; 23 import java.net.URLEncoder;
27 -import java.text.SimpleDateFormat;  
28 -import java.util.*;  
29 -  
30 - 24 +import java.nio.charset.StandardCharsets;
  25 +import java.time.Instant;
31 import java.time.LocalDateTime; 26 import java.time.LocalDateTime;
  27 +import java.time.ZoneId;
  28 +import java.util.*;
32 29
33 import static cn.hutool.crypto.digest.DigestUtil.sha256Hex; 30 import static cn.hutool.crypto.digest.DigestUtil.sha256Hex;
34 import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertList; 31 import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertList;
35 -import static cn.iocoder.yudao.framework.common.util.date.DateUtils.FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND;  
36 -import static cn.iocoder.yudao.framework.common.util.date.DateUtils.TIME_ZONE_DEFAULT;  
37 32
38 -// todo @scholar:参考阿里云在优化下  
39 /** 33 /**
40 * 华为短信客户端的实现类 34 * 华为短信客户端的实现类
41 * 35 *
@@ -45,182 +39,128 @@ import static cn.iocoder.yudao.framework.common.util.date.DateUtils.TIME_ZONE_DE @@ -45,182 +39,128 @@ import static cn.iocoder.yudao.framework.common.util.date.DateUtils.TIME_ZONE_DE
45 @Slf4j 39 @Slf4j
46 public class HuaweiSmsClient extends AbstractSmsClient { 40 public class HuaweiSmsClient extends AbstractSmsClient {
47 41
48 - /**  
49 - * 调用成功 code  
50 - */  
51 - public static final String URL = "https://smsapi.cn-north-4.myhuaweicloud.com:443/sms/batchSendSms/v1";//APP接入地址+接口访问URI  
52 - public static final String HOST = "smsapi.cn-north-4.myhuaweicloud.com:443";  
53 - public static final String SIGNEDHEADERS = "content-type;host;x-sdk-date"; 42 + private static final String URL = "https://smsapi.cn-north-4.myhuaweicloud.com:443/sms/batchSendSms/v1";//APP接入地址+接口访问URI
  43 + private static final String HOST = "smsapi.cn-north-4.myhuaweicloud.com:443";
  44 + private static final String SIGNEDHEADERS = "content-type;host;x-sdk-date";
54 45
55 - @Override  
56 - protected void doInit() {  
57 - } 46 + private static final String RESPONSE_CODE_SUCCESS = "000000";
58 47
59 public HuaweiSmsClient(SmsChannelProperties properties) { 48 public HuaweiSmsClient(SmsChannelProperties properties) {
60 super(properties); 49 super(properties);
61 Assert.notEmpty(properties.getApiKey(), "apiKey 不能为空"); 50 Assert.notEmpty(properties.getApiKey(), "apiKey 不能为空");
62 Assert.notEmpty(properties.getApiSecret(), "apiSecret 不能为空"); 51 Assert.notEmpty(properties.getApiSecret(), "apiSecret 不能为空");
  52 + validateSender(properties);
63 } 53 }
64 54
65 - @Override  
66 - public SmsSendRespDTO sendSms(Long sendLogId, String mobile, String apiTemplateId,  
67 - List<KeyValue<String, Object>> templateParams) throws Throwable {  
68 - // 参考链接 https://support.huaweicloud.com/api-msgsms/sms_05_0001.html  
69 - // 相比较阿里短信,华为短信发送的时候需要额外的参数“通道号”,考虑到不破坏原有的的结构  
70 - // 所以将 通道号 拼接到 apiTemplateId 字段中,格式为 "apiTemplateId 通道号"。空格为分隔符。  
71 - String sender = StrUtil.subAfter(apiTemplateId, " ", true); //中国大陆短信签名通道号或全球短信通道号  
72 - String templateId = StrUtil.subBefore(apiTemplateId, " ", true); //模板ID  
73 -  
74 - //选填,短信状态报告接收地址,推荐使用域名,为空或者不填表示不接收状态报告  
75 - String statusCallBack = properties.getCallbackUrl();  
76 -  
77 - List<String> templateParas = CollectionUtils.convertList(templateParams, kv -> String.valueOf(kv.getValue()));  
78 -  
79 - JSONObject JsonResponse = sendSmsRequest(sender,mobile,templateId,templateParas,statusCallBack);  
80 - SmsResponse smsResponse = getSmsSendResponse(JsonResponse);  
81 -  
82 - return new SmsSendRespDTO().setSuccess(smsResponse.success).setApiMsg(smsResponse.data.toString()); 55 + /**
  56 + * 参数校验华为云的 sender 通道号
  57 + *
  58 + * 原因是:验华为云发放短信的时候,需要额外的参数 sender
  59 + *
  60 + * 解决方案:考虑到不破坏原有的 apiKey + apiSecret 的结构,所以将 secretId 拼接到 apiKey 字段中,格式为 "secretId sdkAppId"。
  61 + *
  62 + * @param properties 配置
  63 + */
  64 + private static void validateSender(SmsChannelProperties properties) {
  65 + String combineKey = properties.getApiKey();
  66 + Assert.notEmpty(combineKey, "apiKey 不能为空");
  67 + String[] keys = combineKey.trim().split(" ");
  68 + Assert.isTrue(keys.length == 2, "华为云短信 apiKey 配置格式错误,请配置 为[accessKeyId sender]");
83 } 69 }
84 70
85 - JSONObject sendSmsRequest(String sender,String mobile,String templateId,List<String> templateParas,String statusCallBack) throws UnsupportedEncodingException {  
86 -  
87 - SimpleDateFormat sdf = new SimpleDateFormat("yyyyMMdd'T'HHmmss'Z'", Locale.ENGLISH);  
88 - sdf.setTimeZone(TimeZone.getTimeZone("UTC"));  
89 - String sdkDate = sdf.format(new Date());  
90 -  
91 - // ************* 步骤 1:拼接规范请求串 *************  
92 - String httpRequestMethod = "POST";  
93 - String canonicalUri = "/sms/batchSendSms/v1/";  
94 - String canonicalQueryString = "";//查询参数为空  
95 - String canonicalHeaders = "content-type:application/x-www-form-urlencoded\n"  
96 - + "host:"+ HOST +"\n"  
97 - + "x-sdk-date:" + sdkDate + "\n";  
98 - //请求Body,不携带签名名称时,signature请填null  
99 - String body = buildRequestBody(sender, mobile, templateId, templateParas, statusCallBack, null);  
100 - if (null == body || body.isEmpty()) {  
101 - return null;  
102 - }  
103 - String hashedRequestBody = sha256Hex(body);  
104 - String canonicalRequest = httpRequestMethod + "\n" + canonicalUri + "\n" + canonicalQueryString + "\n"  
105 - + canonicalHeaders + "\n" + SIGNEDHEADERS + "\n" + hashedRequestBody;  
106 -  
107 - // ************* 步骤 2:拼接待签名字符串 *************  
108 - String hashedCanonicalRequest = sha256Hex(canonicalRequest);  
109 - String stringToSign = "SDK-HMAC-SHA256" + "\n" + sdkDate + "\n" + hashedCanonicalRequest;  
110 -  
111 - // ************* 步骤 3:计算签名 *************  
112 - String signature = SecureUtil.hmacSha256(properties.getApiSecret()).digestHex(stringToSign);  
113 -  
114 - // ************* 步骤 4:拼接 Authorization *************  
115 - String authorization = "SDK-HMAC-SHA256" + " " + "Access=" + properties.getApiKey() + ", "  
116 - + "SignedHeaders=" + SIGNEDHEADERS + ", " + "Signature=" + signature;  
117 -  
118 - // ************* 步骤 5:构造HttpRequest 并执行request请求,获得response *************  
119 - HttpResponse response = HttpRequest.post(URL)  
120 - .header("Content-Type", "application/x-www-form-urlencoded")  
121 - .header("X-Sdk-Date", sdkDate)  
122 - .header("host",HOST)  
123 - .header("Authorization", authorization)  
124 - .body(body)  
125 - .execute();  
126 -  
127 - return JSONUtil.parseObj(response.body()); 71 + private String getAccessKey() {
  72 + return StrUtil.subBefore(properties.getApiKey(), " ", true);
128 } 73 }
129 74
130 - private SmsResponse getSmsSendResponse(JSONObject resJson) {  
131 - SmsResponse smsResponse = new SmsResponse();  
132 - smsResponse.setSuccess("000000".equals(resJson.getStr("code")));  
133 - smsResponse.setData(resJson);  
134 - return smsResponse; 75 + private String getSender() {
  76 + return StrUtil.subAfter(properties.getApiKey(), " ", true);
135 } 77 }
136 78
137 - static String buildRequestBody(String sender, String receiver, String templateId, List<String> templateParas,  
138 - String statusCallBack, String signature) throws UnsupportedEncodingException {  
139 - if (null == sender || null == receiver || null == templateId || sender.isEmpty() || receiver.isEmpty()  
140 - || templateId.isEmpty()) {  
141 - System.out.println("buildRequestBody(): sender, receiver or templateId is null.");  
142 - return null; 79 + @Override
  80 + public SmsSendRespDTO sendSms(Long sendLogId, String mobile, String apiTemplateId,
  81 + List<KeyValue<String, Object>> templateParams) throws Throwable {
  82 + StringBuilder requestBody = new StringBuilder();
  83 + appendToBody(requestBody, "from=", getSender());
  84 + appendToBody(requestBody, "&to=", mobile);
  85 + appendToBody(requestBody, "&templateId=", apiTemplateId);
  86 + appendToBody(requestBody, "&templateParas=", JsonUtils.toJsonString(
  87 + convertList(templateParams, kv -> String.valueOf(kv.getValue()))));
  88 + appendToBody(requestBody, "&statusCallback=", properties.getCallbackUrl());
  89 + appendToBody(requestBody, "&extend=", String.valueOf(sendLogId));
  90 + JSONObject response = request("/sms/batchSendSms/v1/", "POST", requestBody.toString());
  91 +
  92 + // 2. 解析请求
  93 + if (!response.containsKey("result")) { // 例如说:密钥不正确
  94 + return new SmsSendRespDTO().setSuccess(false)
  95 + .setApiCode(response.getStr("code"))
  96 + .setApiMsg(response.getStr("description"));
143 } 97 }
144 -  
145 - StringBuilder body = new StringBuilder();  
146 - appendToBody(body, "from=", sender);  
147 - appendToBody(body, "&to=", receiver);  
148 - appendToBody(body, "&templateId=", templateId);  
149 - appendToBody(body, "&templateParas=", JsonUtils.toJsonString(templateParas));  
150 - appendToBody(body, "&statusCallback=", statusCallBack);  
151 - appendToBody(body, "&signature=", signature);  
152 - return body.toString(); 98 + JSONObject sendResult = response.getJSONArray("result").getJSONObject(0);
  99 + return new SmsSendRespDTO().setSuccess(RESPONSE_CODE_SUCCESS.equals(response.getStr("code")))
  100 + .setSerialNo(sendResult.getStr("smsMsgId")).setApiCode(sendResult.getStr("status"));
153 } 101 }
154 102
155 - private static void appendToBody(StringBuilder body, String key, String val) throws UnsupportedEncodingException {  
156 - if (null != val && !val.isEmpty()) {  
157 - body.append(key).append(URLEncoder.encode(val, "UTF-8"));  
158 - } 103 + /**
  104 + * 请求华为云短信
  105 + *
  106 + * @see <a href="认证鉴权">https://support.huaweicloud.com/api-msgsms/sms_05_0046.html</a>
  107 + * @param uri 请求 URI
  108 + * @param method 请求 Method
  109 + * @param requestBody 请求 Body
  110 + * @return 请求结果
  111 + */
  112 + private JSONObject request(String uri, String method, String requestBody) {
  113 + // 1.1 请求 Header
  114 + TreeMap<String, String> headers = new TreeMap<>();
  115 + headers.put("Content-Type", "application/x-www-form-urlencoded");
  116 + String sdkDate = FastDateFormat.getInstance("yyyyMMdd'T'HHmmss'Z'", TimeZone.getTimeZone("UTC")).format(new Date());
  117 + headers.put("X-Sdk-Date", sdkDate);
  118 + headers.put("host", HOST);
  119 +
  120 + // 1.2 构建签名 Header
  121 + String canonicalQueryString = ""; // 查询参数为空
  122 + String canonicalHeaders = "content-type:application/x-www-form-urlencoded\n"
  123 + + "host:"+ HOST +"\n" + "x-sdk-date:" + sdkDate + "\n";
  124 + String canonicalRequest = method + "\n" + uri + "\n" + canonicalQueryString + "\n"
  125 + + canonicalHeaders + "\n" + SIGNEDHEADERS + "\n" + sha256Hex(requestBody);
  126 + String stringToSign = "SDK-HMAC-SHA256" + "\n" + sdkDate + "\n" + sha256Hex(canonicalRequest);
  127 + String signature = SecureUtil.hmacSha256(properties.getApiSecret()).digestHex(stringToSign); // 计算签名
  128 + headers.put("Authorization", "SDK-HMAC-SHA256" + " " + "Access=" + getAccessKey()
  129 + + ", " + "SignedHeaders=" + SIGNEDHEADERS + ", " + "Signature=" + signature);
  130 +
  131 + // 2. 发起请求
  132 + String responseBody = HttpUtils.post(URL, headers, requestBody);
  133 + return JSONUtil.parseObj(responseBody);
159 } 134 }
  135 +
160 @Override 136 @Override
161 - public List<SmsReceiveRespDTO> parseSmsReceiveStatus(String text) {  
162 - List<SmsReceiveStatus> statuses = JsonUtils.parseArray(text, SmsReceiveStatus.class);  
163 - return convertList(statuses, status -> new SmsReceiveRespDTO().setSuccess(Objects.equals(status.getStatus(),"DELIVRD"))  
164 - .setErrorCode(status.getStatus()).setErrorMsg(status.getStatus())  
165 - .setMobile(status.getPhoneNumber()).setReceiveTime(status.getUpdateTime())  
166 - .setSerialNo(status.getSmsMsgId())); 137 + public List<SmsReceiveRespDTO> parseSmsReceiveStatus(String requestBody) {
  138 + Map<String, String> params = HttpUtil.decodeParamMap(requestBody, StandardCharsets.UTF_8);
  139 + // 字段参考 https://support.huaweicloud.com/api-msgsms/sms_05_0003.html
  140 + return ListUtil.of(new SmsReceiveRespDTO()
  141 + .setSuccess("DELIVRD".equals(params.get("status"))) // 是否接收成功
  142 + .setErrorCode(params.get("status")) // 状态报告编码
  143 + .setErrorMsg(params.get("statusDesc"))
  144 + .setMobile(params.get("to")) // 手机号
  145 + .setReceiveTime(LocalDateTime.ofInstant(Instant.parse(params.get("updateTime")), ZoneId.of("UTC"))) // 状态报告时间
  146 + .setSerialNo(params.get("smsMsgId")) // 发送序列号
  147 + .setLogId(Long.valueOf(params.get("extend")))); // 用户序列号
167 } 148 }
168 149
169 @Override 150 @Override
170 public SmsTemplateRespDTO getSmsTemplate(String apiTemplateId) throws Throwable { 151 public SmsTemplateRespDTO getSmsTemplate(String apiTemplateId) throws Throwable {
171 - //华为短信模板查询和发送短信,是不同的两套key和secret,与阿里、腾讯的区别较大,这里模板查询校验暂不实现。  
172 - return new SmsTemplateRespDTO().setId(null).setContent(null) 152 + // 华为短信模板查询和发送短信,是不同的两套 key 和 secret,与阿里、腾讯的区别较大,这里模板查询校验暂不实现
  153 + String[] strs = apiTemplateId.split(" ");
  154 + Assert.isTrue(strs.length == 2, "格式不正确,需要满足:apiTemplateId sender");
  155 + return new SmsTemplateRespDTO().setId(apiTemplateId).setContent(null)
173 .setAuditStatus(SmsTemplateAuditStatusEnum.SUCCESS.getStatus()).setAuditReason(null); 156 .setAuditStatus(SmsTemplateAuditStatusEnum.SUCCESS.getStatus()).setAuditReason(null);
174 -  
175 - }  
176 -  
177 - @Data  
178 - public static class SmsResponse {  
179 -  
180 - /**  
181 - * 是否成功  
182 - */  
183 - private boolean success;  
184 -  
185 - /**  
186 - * 厂商原返回体  
187 - */  
188 - private Object data;  
189 -  
190 } 157 }
191 158
192 -  
193 - /**  
194 - * 短信接收状态  
195 - *  
196 - * 参见 <a href="https://support.huaweicloud.com/api-msgsms/sms_05_0003.html">文档</a>  
197 - *  
198 - * @author scholar  
199 - */  
200 - @Data  
201 - public static class SmsReceiveStatus {  
202 -  
203 - /**  
204 - * 本条状态报告对应的短信的接收方号码,仅当状态报告中携带了extend参数时才会同时携带该参数  
205 - */  
206 - @JsonProperty("to")  
207 - private String phoneNumber;  
208 -  
209 - /**  
210 - * 短信资源的更新时间,通常为短信平台接收短信状态报告的时间  
211 - */  
212 - @JsonFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND, timezone = TIME_ZONE_DEFAULT)  
213 - private LocalDateTime updateTime;  
214 -  
215 - /**  
216 - * 短信状态报告枚举值  
217 - */  
218 - private String status;  
219 -  
220 - /**  
221 - * 发送短信成功时返回的短信唯一标识。  
222 - */  
223 - private String smsMsgId; 159 + @SuppressWarnings("CharsetObjectCanBeUsed")
  160 + private static void appendToBody(StringBuilder body, String key, String value) throws UnsupportedEncodingException {
  161 + if (StrUtil.isNotEmpty(value)) {
  162 + body.append(key).append(URLEncoder.encode(value, CharsetUtil.CHARSET_UTF_8.name()));
  163 + }
224 } 164 }
225 165
226 -} 166 +}
227 \ No newline at end of file 167 \ No newline at end of file
yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/framework/sms/core/client/impl/QiniuSmsClient.java 0 → 100644
  1 +package cn.iocoder.yudao.module.system.framework.sms.core.client.impl;
  2 +
  3 +import cn.hutool.core.collection.CollStreamUtil;
  4 +import cn.hutool.core.date.DateUtil;
  5 +import cn.hutool.core.date.LocalDateTimeUtil;
  6 +import cn.hutool.core.lang.Assert;
  7 +import cn.hutool.core.util.ObjectUtil;
  8 +import cn.hutool.crypto.SecureUtil;
  9 +import cn.hutool.crypto.digest.HmacAlgorithm;
  10 +import cn.hutool.json.JSONObject;
  11 +import cn.hutool.json.JSONUtil;
  12 +import cn.iocoder.yudao.framework.common.core.KeyValue;
  13 +import cn.iocoder.yudao.framework.common.util.http.HttpUtils;
  14 +import cn.iocoder.yudao.module.system.framework.sms.core.client.dto.SmsReceiveRespDTO;
  15 +import cn.iocoder.yudao.module.system.framework.sms.core.client.dto.SmsSendRespDTO;
  16 +import cn.iocoder.yudao.module.system.framework.sms.core.client.dto.SmsTemplateRespDTO;
  17 +import cn.iocoder.yudao.module.system.framework.sms.core.enums.SmsTemplateAuditStatusEnum;
  18 +import cn.iocoder.yudao.module.system.framework.sms.core.property.SmsChannelProperties;
  19 +import com.google.common.annotations.VisibleForTesting;
  20 +import lombok.extern.slf4j.Slf4j;
  21 +
  22 +import java.util.*;
  23 +import java.util.function.Function;
  24 +
  25 +import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertList;
  26 +
  27 +/**
  28 + * 七牛云短信客户端的实现类
  29 + *
  30 + * @author scholar
  31 + * @since 2024/08/26 15:35
  32 + */
  33 +@Slf4j
  34 +public class QiniuSmsClient extends AbstractSmsClient {
  35 +
  36 + private static final String HOST = "sms.qiniuapi.com";
  37 +
  38 + public QiniuSmsClient(SmsChannelProperties properties) {
  39 + super(properties);
  40 + Assert.notEmpty(properties.getApiKey(), "apiKey 不能为空");
  41 + Assert.notEmpty(properties.getApiSecret(), "apiSecret 不能为空");
  42 + }
  43 +
  44 + public SmsSendRespDTO sendSms(Long sendLogId, String mobile, String apiTemplateId,
  45 + List<KeyValue<String, Object>> templateParams) throws Throwable {
  46 + // 1. 执行请求
  47 + // 参考链接 https://developer.qiniu.com/sms/5824/through-the-api-send-text-messages
  48 + LinkedHashMap<String, Object> body = new LinkedHashMap<>();
  49 + body.put("template_id", apiTemplateId);
  50 + body.put("mobile", mobile);
  51 + body.put("parameters", CollStreamUtil.toMap(templateParams, KeyValue::getKey, KeyValue::getValue));
  52 + body.put("seq", Long.toString(sendLogId));
  53 + JSONObject response = request("POST", body, "/v1/message/single");
  54 +
  55 + // 2. 解析请求
  56 + if (ObjectUtil.isNotEmpty(response.getStr("error"))) {
  57 + // 短信请求失败
  58 + return new SmsSendRespDTO().setSuccess(false)
  59 + .setApiCode(response.getStr("error"))
  60 + .setApiRequestId(response.getStr("request_id"))
  61 + .setApiMsg(response.getStr("message"));
  62 + }
  63 + return new SmsSendRespDTO().setSuccess(response.containsKey("message_id"))
  64 + .setSerialNo(response.getStr("message_id"));
  65 + }
  66 +
  67 + /**
  68 + * 请求七牛云短信
  69 + *
  70 + * @see <a href="https://developer.qiniu.com/sms/5842/sms-api-authentication"</>
  71 + * @param httpMethod http请求方法
  72 + * @param body http请求消息体
  73 + * @param path URL path
  74 + * @return 请求结果
  75 + */
  76 + private JSONObject request(String httpMethod, LinkedHashMap<String, Object> body, String path) {
  77 + String signDate = DateUtil.date().setTimeZone(TimeZone.getTimeZone("UTC")).toString("yyyyMMdd'T'HHmmss'Z'");
  78 + // 1. 请求头
  79 + Map<String, String> header = new HashMap<>(4);
  80 + header.put("HOST", HOST);
  81 + header.put("Authorization", getSignature(httpMethod, path, body != null ? JSONUtil.toJsonStr(body) : "", signDate));
  82 + header.put("Content-Type", "application/json");
  83 + header.put("X-Qiniu-Date", signDate);
  84 +
  85 + // 2. 发起请求
  86 + String responseBody;
  87 + if (Objects.equals(httpMethod, "POST")){
  88 + responseBody = HttpUtils.post("https://" + HOST + path, header, JSONUtil.toJsonStr(body));
  89 + } else {
  90 + responseBody = HttpUtils.get("https://" + HOST + path, header);
  91 + }
  92 + return JSONUtil.parseObj(responseBody);
  93 + }
  94 +
  95 + private String getSignature(String method, String path, String body, String signDate) {
  96 + StringBuilder dataToSign = new StringBuilder();
  97 + dataToSign.append(method.toUpperCase()).append(" ").append(path)
  98 + .append("\nHost: ").append(HOST)
  99 + .append("\n").append("Content-Type").append(": ").append("application/json")
  100 + .append("\n").append("X-Qiniu-Date").append(": ").append(signDate)
  101 + .append("\n\n");
  102 + if (ObjectUtil.isNotEmpty(body)) {
  103 + dataToSign.append(body);
  104 + }
  105 + String signature = SecureUtil.hmac(HmacAlgorithm.HmacSHA1, properties.getApiSecret())
  106 + .digestBase64(dataToSign.toString(), true);
  107 + return "Qiniu " + properties.getApiKey() + ":" + signature;
  108 + }
  109 +
  110 + @Override
  111 + public List<SmsReceiveRespDTO> parseSmsReceiveStatus(String text) {
  112 + JSONObject status = JSONUtil.parseObj(text);
  113 + // 字段参考 https://developer.qiniu.com/sms/5910/message-push
  114 + return convertList(status.getJSONArray("items"), new Function<Object, SmsReceiveRespDTO>() {
  115 +
  116 + @Override
  117 + public SmsReceiveRespDTO apply(Object item) {
  118 + JSONObject statusObj = (JSONObject) item;
  119 + return new SmsReceiveRespDTO()
  120 + .setSuccess("DELIVRD".equals(statusObj.getStr("status"))) // 是否接收成功
  121 + .setErrorMsg(statusObj.getStr("status")) // 状态报告编码
  122 + .setMobile(statusObj.getStr("mobile")) // 手机号
  123 + .setReceiveTime(LocalDateTimeUtil.of(statusObj.getLong("delivrd_at") * 1000L)) // 状态报告时间
  124 + .setSerialNo(statusObj.getStr("message_id")) // 发送序列号
  125 + .setLogId(statusObj.getLong("seq")); // 用户序列号
  126 + }
  127 +
  128 + });
  129 + }
  130 +
  131 + @Override
  132 + public SmsTemplateRespDTO getSmsTemplate(String apiTemplateId) throws Throwable {
  133 + // 1. 执行请求
  134 + // 参考链接 https://developer.qiniu.com/sms/5969/query-a-single-template
  135 + JSONObject response = request("GET", null, "/v1/template/" + apiTemplateId);
  136 +
  137 + // 2.2 解析请求
  138 + return new SmsTemplateRespDTO()
  139 + .setId(response.getStr("id"))
  140 + .setContent(response.getStr("template"))
  141 + .setAuditStatus(convertSmsTemplateAuditStatus(response.getStr("audit_status")))
  142 + .setAuditReason(response.getStr("reject_reason"));
  143 + }
  144 +
  145 + @VisibleForTesting
  146 + Integer convertSmsTemplateAuditStatus(String templateStatus) {
  147 + switch (templateStatus) {
  148 + case "passed": return SmsTemplateAuditStatusEnum.SUCCESS.getStatus();
  149 + case "reviewing": return SmsTemplateAuditStatusEnum.CHECKING.getStatus();
  150 + case "rejected": return SmsTemplateAuditStatusEnum.FAIL.getStatus();
  151 + default:
  152 + throw new IllegalArgumentException(String.format("未知审核状态(%str)", templateStatus));
  153 + }
  154 + }
  155 +}
yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/framework/sms/core/client/impl/SmsClientFactoryImpl.java
@@ -59,7 +59,7 @@ public class SmsClientFactoryImpl implements SmsClientFactory { @@ -59,7 +59,7 @@ public class SmsClientFactoryImpl implements SmsClientFactory {
59 } 59 }
60 60
61 @Override 61 @Override
62 - public void createOrUpdateSmsClient(SmsChannelProperties properties) { 62 + public SmsClient createOrUpdateSmsClient(SmsChannelProperties properties) {
63 AbstractSmsClient client = channelIdClients.get(properties.getId()); 63 AbstractSmsClient client = channelIdClients.get(properties.getId());
64 if (client == null) { 64 if (client == null) {
65 client = this.createSmsClient(properties); 65 client = this.createSmsClient(properties);
@@ -68,6 +68,7 @@ public class SmsClientFactoryImpl implements SmsClientFactory { @@ -68,6 +68,7 @@ public class SmsClientFactoryImpl implements SmsClientFactory {
68 } else { 68 } else {
69 client.refresh(properties); 69 client.refresh(properties);
70 } 70 }
  71 + return client;
71 } 72 }
72 73
73 private AbstractSmsClient createSmsClient(SmsChannelProperties properties) { 74 private AbstractSmsClient createSmsClient(SmsChannelProperties properties) {
@@ -79,6 +80,7 @@ public class SmsClientFactoryImpl implements SmsClientFactory { @@ -79,6 +80,7 @@ public class SmsClientFactoryImpl implements SmsClientFactory {
79 case DEBUG_DING_TALK: return new DebugDingTalkSmsClient(properties); 80 case DEBUG_DING_TALK: return new DebugDingTalkSmsClient(properties);
80 case TENCENT: return new TencentSmsClient(properties); 81 case TENCENT: return new TencentSmsClient(properties);
81 case HUAWEI: return new HuaweiSmsClient(properties); 82 case HUAWEI: return new HuaweiSmsClient(properties);
  83 + case QINIU: return new QiniuSmsClient(properties);
82 } 84 }
83 // 创建失败,错误日志 + 抛出异常 85 // 创建失败,错误日志 + 抛出异常
84 log.error("[createSmsClient][配置({}) 找不到合适的客户端实现]", properties); 86 log.error("[createSmsClient][配置({}) 找不到合适的客户端实现]", properties);
yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/framework/sms/core/client/impl/TencentSmsClient.java
1 package cn.iocoder.yudao.module.system.framework.sms.core.client.impl; 1 package cn.iocoder.yudao.module.system.framework.sms.core.client.impl;
2 2
  3 +import cn.hutool.core.date.format.FastDateFormat;
3 import cn.hutool.core.lang.Assert; 4 import cn.hutool.core.lang.Assert;
  5 +import cn.hutool.core.util.HexUtil;
4 import cn.hutool.core.util.StrUtil; 6 import cn.hutool.core.util.StrUtil;
  7 +import cn.hutool.crypto.digest.DigestUtil;
  8 +import cn.hutool.crypto.digest.HmacAlgorithm;
5 import cn.hutool.json.JSONArray; 9 import cn.hutool.json.JSONArray;
6 import cn.hutool.json.JSONObject; 10 import cn.hutool.json.JSONObject;
7 import cn.hutool.json.JSONUtil; 11 import cn.hutool.json.JSONUtil;
@@ -14,12 +18,8 @@ import cn.iocoder.yudao.module.system.framework.sms.core.client.dto.SmsTemplateR @@ -14,12 +18,8 @@ import cn.iocoder.yudao.module.system.framework.sms.core.client.dto.SmsTemplateR
14 import cn.iocoder.yudao.module.system.framework.sms.core.enums.SmsTemplateAuditStatusEnum; 18 import cn.iocoder.yudao.module.system.framework.sms.core.enums.SmsTemplateAuditStatusEnum;
15 import cn.iocoder.yudao.module.system.framework.sms.core.property.SmsChannelProperties; 19 import cn.iocoder.yudao.module.system.framework.sms.core.property.SmsChannelProperties;
16 import com.google.common.annotations.VisibleForTesting; 20 import com.google.common.annotations.VisibleForTesting;
17 -import jakarta.xml.bind.DatatypeConverter;  
18 21
19 -import javax.crypto.Mac;  
20 -import javax.crypto.spec.SecretKeySpec;  
21 import java.nio.charset.StandardCharsets; 22 import java.nio.charset.StandardCharsets;
22 -import java.text.SimpleDateFormat;  
23 import java.util.*; 23 import java.util.*;
24 24
25 import static cn.hutool.crypto.digest.DigestUtil.sha256Hex; 25 import static cn.hutool.crypto.digest.DigestUtil.sha256Hex;
@@ -34,6 +34,7 @@ import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils. @@ -34,6 +34,7 @@ import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.
34 */ 34 */
35 public class TencentSmsClient extends AbstractSmsClient { 35 public class TencentSmsClient extends AbstractSmsClient {
36 36
  37 + private static final String HOST = "sms.tencentcloudapi.com";
37 private static final String VERSION = "2021-01-11"; 38 private static final String VERSION = "2021-01-11";
38 private static final String REGION = "ap-guangzhou"; 39 private static final String REGION = "ap-guangzhou";
39 40
@@ -56,10 +57,6 @@ public class TencentSmsClient extends AbstractSmsClient { @@ -56,10 +57,6 @@ public class TencentSmsClient extends AbstractSmsClient {
56 validateSdkAppId(properties); 57 validateSdkAppId(properties);
57 } 58 }
58 59
59 - @Override  
60 - protected void doInit() {  
61 - }  
62 -  
63 /** 60 /**
64 * 参数校验腾讯云的 SDK AppId 61 * 参数校验腾讯云的 SDK AppId
65 * 62 *
@@ -93,7 +90,7 @@ public class TencentSmsClient extends AbstractSmsClient { @@ -93,7 +90,7 @@ public class TencentSmsClient extends AbstractSmsClient {
93 body.put("PhoneNumberSet", new String[]{mobile}); 90 body.put("PhoneNumberSet", new String[]{mobile});
94 body.put("SmsSdkAppId", getSdkAppId()); 91 body.put("SmsSdkAppId", getSdkAppId());
95 body.put("SignName", properties.getSignature()); 92 body.put("SignName", properties.getSignature());
96 - body.put("TemplateId",apiTemplateId); 93 + body.put("TemplateId", apiTemplateId);
97 body.put("TemplateParamSet", ArrayUtils.toArray(templateParams, param -> String.valueOf(param.getValue()))); 94 body.put("TemplateParamSet", ArrayUtils.toArray(templateParams, param -> String.valueOf(param.getValue())));
98 JSONObject response = request("SendSms", body); 95 JSONObject response = request("SendSms", body);
99 96
@@ -106,11 +103,11 @@ public class TencentSmsClient extends AbstractSmsClient { @@ -106,11 +103,11 @@ public class TencentSmsClient extends AbstractSmsClient {
106 .setApiCode(error.getStr("Code")) 103 .setApiCode(error.getStr("Code"))
107 .setApiMsg(error.getStr("Message")); 104 .setApiMsg(error.getStr("Message"));
108 } 105 }
109 - JSONObject responseData = responseResult.getJSONArray("SendStatusSet").getJSONObject(0);  
110 - return new SmsSendRespDTO().setSuccess(Objects.equals(API_CODE_SUCCESS, responseData.getStr("Code"))) 106 + JSONObject sendResult = responseResult.getJSONArray("SendStatusSet").getJSONObject(0);
  107 + return new SmsSendRespDTO().setSuccess(Objects.equals(API_CODE_SUCCESS, sendResult.getStr("Code")))
111 .setApiRequestId(responseResult.getStr("RequestId")) 108 .setApiRequestId(responseResult.getStr("RequestId"))
112 - .setSerialNo(responseData.getStr("SerialNo"))  
113 - .setApiMsg(responseData.getStr("Message")); 109 + .setSerialNo(sendResult.getStr("SerialNo"))
  110 + .setApiMsg(sendResult.getStr("Message"));
114 } 111 }
115 112
116 @Override 113 @Override
@@ -137,14 +134,13 @@ public class TencentSmsClient extends AbstractSmsClient { @@ -137,14 +134,13 @@ public class TencentSmsClient extends AbstractSmsClient {
137 body.put("TemplateIdSet", new Integer[]{Integer.valueOf(apiTemplateId)}); 134 body.put("TemplateIdSet", new Integer[]{Integer.valueOf(apiTemplateId)});
138 JSONObject response = request("DescribeSmsTemplateList", body); 135 JSONObject response = request("DescribeSmsTemplateList", body);
139 136
140 - // TODO @scholar:会有请求失败的情况么?类似发送的(那块逻辑我补充了)  
141 - JSONObject TemplateStatusSet = response.getJSONObject("Response").getJSONArray("DescribeTemplateStatusSet").getJSONObject(0);  
142 - String content = TemplateStatusSet.get("TemplateContent").toString();  
143 - int templateStatus = Integer.parseInt(TemplateStatusSet.get("StatusCode").toString());  
144 - String auditReason = TemplateStatusSet.get("ReviewReply").toString();  
145 -  
146 - return new SmsTemplateRespDTO().setId(apiTemplateId).setContent(content)  
147 - .setAuditStatus(convertSmsTemplateAuditStatus(templateStatus)).setAuditReason(auditReason); 137 + // 2. 解析请求
  138 + JSONObject statusResult = response.getJSONObject("Response")
  139 + .getJSONArray("DescribeTemplateStatusSet").getJSONObject(0);
  140 + return new SmsTemplateRespDTO().setId(apiTemplateId)
  141 + .setContent(statusResult.get("TemplateContent").toString())
  142 + .setAuditStatus(convertSmsTemplateAuditStatus(statusResult.getInt("StatusCode")))
  143 + .setAuditReason(statusResult.get("ReviewReply").toString());
148 } 144 }
149 145
150 @VisibleForTesting 146 @VisibleForTesting
@@ -166,64 +162,40 @@ public class TencentSmsClient extends AbstractSmsClient { @@ -166,64 +162,40 @@ public class TencentSmsClient extends AbstractSmsClient {
166 * @param body 请求参数 162 * @param body 请求参数
167 * @return 请求结果 163 * @return 请求结果
168 */ 164 */
169 - private JSONObject request(String action, TreeMap<String, Object> body) throws Exception {  
170 - String timestamp = String.valueOf(System.currentTimeMillis() / 1000);  
171 - // TODO @scholar:这个 format,看看怎么写的可以简化点  
172 - SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd");  
173 - // 注意时区,否则容易出错  
174 - sdf.setTimeZone(TimeZone.getTimeZone("UTC"));  
175 - String date = sdf.format(new Date(Long.valueOf(timestamp + "000")));  
176 -  
177 - // TODO @scholar:这个步骤,看看怎么参考阿里云 client,归类下;1. 2.1 2.2 这种  
178 - // ************* 步骤 1:拼接规范请求串 *************  
179 - // TODO @scholar:这个 hsot 枚举下;  
180 - String host = "sms.tencentcloudapi.com"; //APP接入地址+接口访问URI  
181 - String httpMethod = "POST"; // 请求方式  
182 - String canonicalUri = "/";  
183 - String canonicalQueryString = "";  
184 -  
185 - String canonicalHeaders = "content-type:application/json; charset=utf-8\n"  
186 - + "host:" + host + "\n" + "x-tc-action:" + action.toLowerCase() + "\n";  
187 - String signedHeaders = "content-type;host;x-tc-action";  
188 - String hashedRequestBody = sha256Hex(JSONUtil.toJsonStr(body));  
189 - // TODO @scholar:换行下,不然单行太长了  
190 - String canonicalRequest = httpMethod + "\n" + canonicalUri + "\n" + canonicalQueryString + "\n" + canonicalHeaders + "\n" + signedHeaders + "\n" + hashedRequestBody;  
191 -  
192 - // ************* 步骤 2:拼接待签名字符串 *************  
193 - String credentialScope = date + "/" + "sms" + "/" + "tc3_request";  
194 - String hashedCanonicalRequest = sha256Hex(canonicalRequest);  
195 - String stringToSign = "TC3-HMAC-SHA256" + "\n" + timestamp + "\n" + credentialScope + "\n" + hashedCanonicalRequest;  
196 -  
197 - // ************* 步骤 3:计算签名 *************  
198 - byte[] secretDate = hmac256(("TC3" + properties.getApiSecret()).getBytes(StandardCharsets.UTF_8), date);  
199 - byte[] secretService = hmac256(secretDate, "sms");  
200 - byte[] secretSigning = hmac256(secretService, "tc3_request");  
201 - String signature = DatatypeConverter.printHexBinary(hmac256(secretSigning, stringToSign)).toLowerCase();  
202 -  
203 - // ************* 步骤 4:拼接 Authorization *************  
204 - String authorization = "TC3-HMAC-SHA256" + " " + "Credential=" + getApiKey() + "/" + credentialScope + ", "  
205 - + "SignedHeaders=" + signedHeaders + ", " + "Signature=" + signature;  
206 -  
207 - // ************* 步骤 5:构造HttpRequest 并执行request请求,获得response ************* 165 + private JSONObject request(String action, TreeMap<String, Object> body) {
  166 + // 1.1 请求 Header
208 Map<String, String> headers = new HashMap<>(); 167 Map<String, String> headers = new HashMap<>();
209 - headers.put("Authorization", authorization);  
210 headers.put("Content-Type", "application/json; charset=utf-8"); 168 headers.put("Content-Type", "application/json; charset=utf-8");
211 - headers.put("Host", host); 169 + headers.put("Host", HOST);
212 headers.put("X-TC-Action", action); 170 headers.put("X-TC-Action", action);
213 - headers.put("X-TC-Timestamp", timestamp); 171 + Date now = new Date();
  172 + String nowStr = FastDateFormat.getInstance("yyyy-MM-dd", TimeZone.getTimeZone("UTC")).format(now);
  173 + headers.put("X-TC-Timestamp", String.valueOf(now.getTime() / 1000));
214 headers.put("X-TC-Version", VERSION); 174 headers.put("X-TC-Version", VERSION);
215 headers.put("X-TC-Region", REGION); 175 headers.put("X-TC-Region", REGION);
216 176
217 - String responseBody = HttpUtils.post("https://" + host, headers, JSONUtil.toJsonStr(body));  
218 - 177 + // 1.2 构建签名 Header
  178 + String canonicalQueryString = "";
  179 + String canonicalHeaders = "content-type:application/json; charset=utf-8\n"
  180 + + "host:" + HOST + "\n" + "x-tc-action:" + action.toLowerCase() + "\n";
  181 + String signedHeaders = "content-type;host;x-tc-action";
  182 + String canonicalRequest = "POST" + "\n" + "/" + "\n" + canonicalQueryString + "\n" + canonicalHeaders + "\n"
  183 + + signedHeaders + "\n" + sha256Hex(JSONUtil.toJsonStr(body));
  184 + String credentialScope = nowStr + "/" + "sms" + "/" + "tc3_request";
  185 + String stringToSign = "TC3-HMAC-SHA256" + "\n" + now.getTime() / 1000 + "\n" + credentialScope + "\n" +
  186 + sha256Hex(canonicalRequest);
  187 + byte[] secretService = hmac256(hmac256(("TC3" + properties.getApiSecret()).getBytes(StandardCharsets.UTF_8), nowStr), "sms");
  188 + String signature = HexUtil.encodeHexStr(hmac256(hmac256(secretService, "tc3_request"), stringToSign));
  189 + headers.put("Authorization", "TC3-HMAC-SHA256" + " " + "Credential=" + getApiKey() + "/" + credentialScope + ", "
  190 + + "SignedHeaders=" + signedHeaders + ", " + "Signature=" + signature);
  191 +
  192 + // 2. 发起请求
  193 + String responseBody = HttpUtils.post("https://" + HOST, headers, JSONUtil.toJsonStr(body));
219 return JSONUtil.parseObj(responseBody); 194 return JSONUtil.parseObj(responseBody);
220 } 195 }
221 196
222 - // TODO @scholar:使用 hutool 简化下  
223 - private static byte[] hmac256(byte[] key, String msg) throws Exception {  
224 - Mac mac = Mac.getInstance("HmacSHA256");  
225 - SecretKeySpec secretKeySpec = new SecretKeySpec(key, mac.getAlgorithm());  
226 - mac.init(secretKeySpec);  
227 - return mac.doFinal(msg.getBytes(StandardCharsets.UTF_8)); 197 + private static byte[] hmac256(byte[] key, String msg) {
  198 + return DigestUtil.hmac(HmacAlgorithm.HmacSHA256, key).digest(msg);
228 } 199 }
  200 +
229 } 201 }
230 \ No newline at end of file 202 \ No newline at end of file
yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/framework/sms/core/enums/SmsChannelEnum.java
@@ -18,6 +18,7 @@ public enum SmsChannelEnum { @@ -18,6 +18,7 @@ public enum SmsChannelEnum {
18 ALIYUN("ALIYUN", "阿里云"), 18 ALIYUN("ALIYUN", "阿里云"),
19 TENCENT("TENCENT", "腾讯云"), 19 TENCENT("TENCENT", "腾讯云"),
20 HUAWEI("HUAWEI", "华为云"), 20 HUAWEI("HUAWEI", "华为云"),
  21 + QINIU("QINIU", "七牛云"),
21 ; 22 ;
22 23
23 /** 24 /**
@@ -34,3 +35,4 @@ public enum SmsChannelEnum { @@ -34,3 +35,4 @@ public enum SmsChannelEnum {
34 } 35 }
35 36
36 } 37 }
  38 +
yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/service/oauth2/OAuth2TokenServiceImpl.java
@@ -56,7 +56,7 @@ public class OAuth2TokenServiceImpl implements OAuth2TokenService { @@ -56,7 +56,7 @@ public class OAuth2TokenServiceImpl implements OAuth2TokenService {
56 private AdminUserService adminUserService; 56 private AdminUserService adminUserService;
57 57
58 @Override 58 @Override
59 - @Transactional 59 + @Transactional(rollbackFor = Exception.class)
60 public OAuth2AccessTokenDO createAccessToken(Long userId, Integer userType, String clientId, List<String> scopes) { 60 public OAuth2AccessTokenDO createAccessToken(Long userId, Integer userType, String clientId, List<String> scopes) {
61 OAuth2ClientDO clientDO = oauth2ClientService.validOAuthClientFromCache(clientId); 61 OAuth2ClientDO clientDO = oauth2ClientService.validOAuthClientFromCache(clientId);
62 // 创建刷新令牌 62 // 创建刷新令牌
@@ -66,6 +66,7 @@ public class OAuth2TokenServiceImpl implements OAuth2TokenService { @@ -66,6 +66,7 @@ public class OAuth2TokenServiceImpl implements OAuth2TokenService {
66 } 66 }
67 67
68 @Override 68 @Override
  69 + @Transactional(rollbackFor = Exception.class)
69 public OAuth2AccessTokenDO refreshAccessToken(String refreshToken, String clientId) { 70 public OAuth2AccessTokenDO refreshAccessToken(String refreshToken, String clientId) {
70 // 查询访问令牌 71 // 查询访问令牌
71 OAuth2RefreshTokenDO refreshTokenDO = oauth2RefreshTokenMapper.selectByRefreshToken(refreshToken); 72 OAuth2RefreshTokenDO refreshTokenDO = oauth2RefreshTokenMapper.selectByRefreshToken(refreshToken);
@@ -82,7 +83,7 @@ public class OAuth2TokenServiceImpl implements OAuth2TokenService { @@ -82,7 +83,7 @@ public class OAuth2TokenServiceImpl implements OAuth2TokenService {
82 // 移除相关的访问令牌 83 // 移除相关的访问令牌
83 List<OAuth2AccessTokenDO> accessTokenDOs = oauth2AccessTokenMapper.selectListByRefreshToken(refreshToken); 84 List<OAuth2AccessTokenDO> accessTokenDOs = oauth2AccessTokenMapper.selectListByRefreshToken(refreshToken);
84 if (CollUtil.isNotEmpty(accessTokenDOs)) { 85 if (CollUtil.isNotEmpty(accessTokenDOs)) {
85 - oauth2AccessTokenMapper.deleteBatchIds(convertSet(accessTokenDOs, OAuth2AccessTokenDO::getId)); 86 + oauth2AccessTokenMapper.deleteByIds(convertSet(accessTokenDOs, OAuth2AccessTokenDO::getId));
86 oauth2AccessTokenRedisDAO.deleteList(convertSet(accessTokenDOs, OAuth2AccessTokenDO::getAccessToken)); 87 oauth2AccessTokenRedisDAO.deleteList(convertSet(accessTokenDOs, OAuth2AccessTokenDO::getAccessToken));
87 } 88 }
88 89
@@ -126,6 +127,7 @@ public class OAuth2TokenServiceImpl implements OAuth2TokenService { @@ -126,6 +127,7 @@ public class OAuth2TokenServiceImpl implements OAuth2TokenService {
126 } 127 }
127 128
128 @Override 129 @Override
  130 + @Transactional(rollbackFor = Exception.class)
129 public OAuth2AccessTokenDO removeAccessToken(String accessToken) { 131 public OAuth2AccessTokenDO removeAccessToken(String accessToken) {
130 // 删除访问令牌 132 // 删除访问令牌
131 OAuth2AccessTokenDO accessTokenDO = oauth2AccessTokenMapper.selectByAccessToken(accessToken); 133 OAuth2AccessTokenDO accessTokenDO = oauth2AccessTokenMapper.selectByAccessToken(accessToken);
yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/service/sms/SmsChannelServiceImpl.java
1 package cn.iocoder.yudao.module.system.service.sms; 1 package cn.iocoder.yudao.module.system.service.sms;
2 2
3 -import cn.hutool.core.util.StrUtil;  
4 import cn.iocoder.yudao.framework.common.pojo.PageResult; 3 import cn.iocoder.yudao.framework.common.pojo.PageResult;
5 import cn.iocoder.yudao.framework.common.util.object.BeanUtils; 4 import cn.iocoder.yudao.framework.common.util.object.BeanUtils;
6 -import cn.iocoder.yudao.module.system.framework.sms.core.client.SmsClient;  
7 -import cn.iocoder.yudao.module.system.framework.sms.core.client.SmsClientFactory;  
8 -import cn.iocoder.yudao.module.system.framework.sms.core.property.SmsChannelProperties;  
9 import cn.iocoder.yudao.module.system.controller.admin.sms.vo.channel.SmsChannelPageReqVO; 5 import cn.iocoder.yudao.module.system.controller.admin.sms.vo.channel.SmsChannelPageReqVO;
10 import cn.iocoder.yudao.module.system.controller.admin.sms.vo.channel.SmsChannelSaveReqVO; 6 import cn.iocoder.yudao.module.system.controller.admin.sms.vo.channel.SmsChannelSaveReqVO;
11 import cn.iocoder.yudao.module.system.dal.dataobject.sms.SmsChannelDO; 7 import cn.iocoder.yudao.module.system.dal.dataobject.sms.SmsChannelDO;
12 import cn.iocoder.yudao.module.system.dal.mysql.sms.SmsChannelMapper; 8 import cn.iocoder.yudao.module.system.dal.mysql.sms.SmsChannelMapper;
13 -import com.google.common.cache.CacheLoader;  
14 -import com.google.common.cache.LoadingCache;  
15 -import lombok.Getter; 9 +import cn.iocoder.yudao.module.system.framework.sms.core.client.SmsClient;
  10 +import cn.iocoder.yudao.module.system.framework.sms.core.client.SmsClientFactory;
  11 +import cn.iocoder.yudao.module.system.framework.sms.core.property.SmsChannelProperties;
  12 +import jakarta.annotation.Resource;
16 import lombok.extern.slf4j.Slf4j; 13 import lombok.extern.slf4j.Slf4j;
17 import org.springframework.stereotype.Service; 14 import org.springframework.stereotype.Service;
18 15
19 -import jakarta.annotation.Resource;  
20 -import java.time.Duration;  
21 import java.util.List; 16 import java.util.List;
22 17
23 import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception; 18 import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception;
24 -import static cn.iocoder.yudao.framework.common.util.cache.CacheUtils.buildAsyncReloadingCache;  
25 import static cn.iocoder.yudao.module.system.enums.ErrorCodeConstants.SMS_CHANNEL_HAS_CHILDREN; 19 import static cn.iocoder.yudao.module.system.enums.ErrorCodeConstants.SMS_CHANNEL_HAS_CHILDREN;
26 import static cn.iocoder.yudao.module.system.enums.ErrorCodeConstants.SMS_CHANNEL_NOT_EXISTS; 20 import static cn.iocoder.yudao.module.system.enums.ErrorCodeConstants.SMS_CHANNEL_NOT_EXISTS;
27 21
@@ -34,46 +28,6 @@ import static cn.iocoder.yudao.module.system.enums.ErrorCodeConstants.SMS_CHANNE @@ -34,46 +28,6 @@ import static cn.iocoder.yudao.module.system.enums.ErrorCodeConstants.SMS_CHANNE
34 @Slf4j 28 @Slf4j
35 public class SmsChannelServiceImpl implements SmsChannelService { 29 public class SmsChannelServiceImpl implements SmsChannelService {
36 30
37 - /**  
38 - * {@link SmsClient} 缓存,通过它异步刷新 smsClientFactory  
39 - */  
40 - @Getter  
41 - private final LoadingCache<Long, SmsClient> idClientCache = buildAsyncReloadingCache(Duration.ofSeconds(10L),  
42 - new CacheLoader<Long, SmsClient>() {  
43 -  
44 - @Override  
45 - public SmsClient load(Long id) {  
46 - // 查询,然后尝试刷新  
47 - SmsChannelDO channel = smsChannelMapper.selectById(id);  
48 - if (channel != null) {  
49 - SmsChannelProperties properties = BeanUtils.toBean(channel, SmsChannelProperties.class);  
50 - smsClientFactory.createOrUpdateSmsClient(properties);  
51 - }  
52 - return smsClientFactory.getSmsClient(id);  
53 - }  
54 -  
55 - });  
56 -  
57 - /**  
58 - * {@link SmsClient} 缓存,通过它异步刷新 smsClientFactory  
59 - */  
60 - @Getter  
61 - private final LoadingCache<String, SmsClient> codeClientCache = buildAsyncReloadingCache(Duration.ofSeconds(60L),  
62 - new CacheLoader<String, SmsClient>() {  
63 -  
64 - @Override  
65 - public SmsClient load(String code) {  
66 - // 查询,然后尝试刷新  
67 - SmsChannelDO channel = smsChannelMapper.selectByCode(code);  
68 - if (channel != null) {  
69 - SmsChannelProperties properties = BeanUtils.toBean(channel, SmsChannelProperties.class);  
70 - smsClientFactory.createOrUpdateSmsClient(properties);  
71 - }  
72 - return smsClientFactory.getSmsClient(code);  
73 - }  
74 -  
75 - });  
76 -  
77 @Resource 31 @Resource
78 private SmsClientFactory smsClientFactory; 32 private SmsClientFactory smsClientFactory;
79 33
@@ -93,41 +47,22 @@ public class SmsChannelServiceImpl implements SmsChannelService { @@ -93,41 +47,22 @@ public class SmsChannelServiceImpl implements SmsChannelService {
93 @Override 47 @Override
94 public void updateSmsChannel(SmsChannelSaveReqVO updateReqVO) { 48 public void updateSmsChannel(SmsChannelSaveReqVO updateReqVO) {
95 // 校验存在 49 // 校验存在
96 - SmsChannelDO channel = validateSmsChannelExists(updateReqVO.getId()); 50 + validateSmsChannelExists(updateReqVO.getId());
97 // 更新 51 // 更新
98 SmsChannelDO updateObj = BeanUtils.toBean(updateReqVO, SmsChannelDO.class); 52 SmsChannelDO updateObj = BeanUtils.toBean(updateReqVO, SmsChannelDO.class);
99 smsChannelMapper.updateById(updateObj); 53 smsChannelMapper.updateById(updateObj);
100 -  
101 - // 清空缓存  
102 - clearCache(updateReqVO.getId(), channel.getCode());  
103 } 54 }
104 55
105 @Override 56 @Override
106 public void deleteSmsChannel(Long id) { 57 public void deleteSmsChannel(Long id) {
107 // 校验存在 58 // 校验存在
108 - SmsChannelDO channel = validateSmsChannelExists(id); 59 + validateSmsChannelExists(id);
109 // 校验是否有在使用该账号的模版 60 // 校验是否有在使用该账号的模版
110 if (smsTemplateService.getSmsTemplateCountByChannelId(id) > 0) { 61 if (smsTemplateService.getSmsTemplateCountByChannelId(id) > 0) {
111 throw exception(SMS_CHANNEL_HAS_CHILDREN); 62 throw exception(SMS_CHANNEL_HAS_CHILDREN);
112 } 63 }
113 // 删除 64 // 删除
114 smsChannelMapper.deleteById(id); 65 smsChannelMapper.deleteById(id);
115 -  
116 - // 清空缓存  
117 - clearCache(id, channel.getCode());  
118 - }  
119 -  
120 - /**  
121 - * 清空指定渠道编号的缓存  
122 - *  
123 - * @param id 渠道编号  
124 - * @param code 渠道编码  
125 - */  
126 - private void clearCache(Long id, String code) {  
127 - idClientCache.invalidate(id);  
128 - if (StrUtil.isNotEmpty(code)) {  
129 - codeClientCache.invalidate(code);  
130 - }  
131 } 66 }
132 67
133 private SmsChannelDO validateSmsChannelExists(Long id) { 68 private SmsChannelDO validateSmsChannelExists(Long id) {
@@ -155,12 +90,14 @@ public class SmsChannelServiceImpl implements SmsChannelService { @@ -155,12 +90,14 @@ public class SmsChannelServiceImpl implements SmsChannelService {
155 90
156 @Override 91 @Override
157 public SmsClient getSmsClient(Long id) { 92 public SmsClient getSmsClient(Long id) {
158 - return idClientCache.getUnchecked(id); 93 + SmsChannelDO channel = smsChannelMapper.selectById(id);
  94 + SmsChannelProperties properties = BeanUtils.toBean(channel, SmsChannelProperties.class);
  95 + return smsClientFactory.createOrUpdateSmsClient(properties);
159 } 96 }
160 97
161 @Override 98 @Override
162 public SmsClient getSmsClient(String code) { 99 public SmsClient getSmsClient(String code) {
163 - return codeClientCache.getUnchecked(code); 100 + return smsClientFactory.getSmsClient(code);
164 } 101 }
165 102
166 } 103 }
yudao-module-system/yudao-module-system-biz/src/test/java/cn/iocoder/yudao/module/system/framework/sms/core/client/impl/HuaweiSmsClientTest.java 0 → 100644
  1 +package cn.iocoder.yudao.module.system.framework.sms.core.client.impl;
  2 +
  3 +import cn.iocoder.yudao.framework.common.core.KeyValue;
  4 +import cn.iocoder.yudao.framework.common.util.http.HttpUtils;
  5 +import cn.iocoder.yudao.framework.test.core.ut.BaseMockitoUnitTest;
  6 +import cn.iocoder.yudao.module.system.framework.sms.core.client.dto.SmsReceiveRespDTO;
  7 +import cn.iocoder.yudao.module.system.framework.sms.core.client.dto.SmsSendRespDTO;
  8 +import cn.iocoder.yudao.module.system.framework.sms.core.property.SmsChannelProperties;
  9 +import com.google.common.collect.Lists;
  10 +
  11 +import org.junit.jupiter.api.Test;
  12 +import org.mockito.InjectMocks;
  13 +import org.mockito.MockedStatic;
  14 +
  15 +import java.time.LocalDateTime;
  16 +import java.util.List;
  17 +
  18 +import static cn.iocoder.yudao.framework.test.core.util.RandomUtils.*;
  19 +import static org.junit.jupiter.api.Assertions.*;
  20 +import static org.mockito.ArgumentMatchers.*;
  21 +import static org.mockito.ArgumentMatchers.anyString;
  22 +import static org.mockito.Mockito.mockStatic;
  23 +
  24 +/**
  25 + * {@link HuaweiSmsClient} 的单元测试
  26 + *
  27 + * @author scholar
  28 + */
  29 +public class HuaweiSmsClientTest extends BaseMockitoUnitTest {
  30 +
  31 + private final SmsChannelProperties properties = new SmsChannelProperties()
  32 + .setApiKey(randomString() + " " + randomString()) // 随机一个 apiKey,避免构建报错
  33 + .setApiSecret(randomString()) // 随机一个 apiSecret,避免构建报错
  34 + .setSignature("芋道源码");
  35 +
  36 + @InjectMocks
  37 + private HuaweiSmsClient smsClient = new HuaweiSmsClient(properties);
  38 +
  39 + @Test
  40 + public void testDoSendSms_success() throws Throwable {
  41 + try (MockedStatic<HttpUtils> httpUtilsMockedStatic = mockStatic(HttpUtils.class)) {
  42 + // 准备参数
  43 + Long sendLogId = randomLongId();
  44 + String mobile = randomString();
  45 + String apiTemplateId = randomString() + " " + randomString();
  46 + List<KeyValue<String, Object>> templateParams = Lists.newArrayList(
  47 + new KeyValue<>("1", 1234), new KeyValue<>("2", "login"));
  48 +
  49 + // mock 方法
  50 + httpUtilsMockedStatic.when(() -> HttpUtils.post(anyString(), anyMap(), anyString()))
  51 + .thenReturn("{\"result\":[{\"originTo\":\"+86155****5678\",\"createTime\":\"2018-05-25T16:34:34Z\",\"from\":\"1069********0012\",\"smsMsgId\":\"d6e3cdd0-522b-4692-8304-a07553cdf591_8539659\",\"status\":\"000000\",\"countryId\":\"CN\",\"total\":2}],\"code\":\"000000\",\"description\":\"Success\"}\n");
  52 +
  53 + // 调用
  54 + SmsSendRespDTO result = smsClient.sendSms(sendLogId, mobile,
  55 + apiTemplateId, templateParams);
  56 + // 断言
  57 + assertTrue(result.getSuccess());
  58 + assertEquals("d6e3cdd0-522b-4692-8304-a07553cdf591_8539659", result.getSerialNo());
  59 + assertEquals("000000", result.getApiCode());
  60 + }
  61 + }
  62 +
  63 + @Test
  64 + public void testDoSendSms_fail_01() throws Throwable {
  65 + try (MockedStatic<HttpUtils> httpUtilsMockedStatic = mockStatic(HttpUtils.class)) {
  66 + // 准备参数
  67 + Long sendLogId = randomLongId();
  68 + String mobile = randomString();
  69 + String apiTemplateId = randomString() + " " + randomString();
  70 + List<KeyValue<String, Object>> templateParams = Lists.newArrayList(
  71 + new KeyValue<>("1", 1234), new KeyValue<>("2", "login"));
  72 +
  73 + // mock 方法
  74 + httpUtilsMockedStatic.when(() -> HttpUtils.post(anyString(), anyMap(), anyString()))
  75 + .thenReturn("{\"result\":[{\"total\":1,\"originTo\":\"17321315478\",\"createTime\":\"2024-08-18T11:32:20Z\",\"from\":\"x8824060312575\",\"smsMsgId\":\"06e4b966-ad87-479f-8b74-f57fb7aafb60_304613461\",\"countryId\":\"CN\",\"status\":\"E200033\"}],\"code\":\"E000510\",\"description\":\"The SMS fails to be sent. For details, see status.\"}");
  76 +
  77 + // 调用
  78 + SmsSendRespDTO result = smsClient.sendSms(sendLogId, mobile,
  79 + apiTemplateId, templateParams);
  80 + // 断言
  81 + assertFalse(result.getSuccess());
  82 + assertEquals("06e4b966-ad87-479f-8b74-f57fb7aafb60_304613461", result.getSerialNo());
  83 + assertEquals("E200033", result.getApiCode());
  84 + }
  85 + }
  86 +
  87 + @Test
  88 + public void testDoSendSms_fail_02() throws Throwable {
  89 + try (MockedStatic<HttpUtils> httpUtilsMockedStatic = mockStatic(HttpUtils.class)) {
  90 + // 准备参数
  91 + Long sendLogId = randomLongId();
  92 + String mobile = randomString();
  93 + String apiTemplateId = randomString() + " " + randomString();
  94 + List<KeyValue<String, Object>> templateParams = Lists.newArrayList(
  95 + new KeyValue<>("1", 1234), new KeyValue<>("2", "login"));
  96 +
  97 + // mock 方法
  98 + httpUtilsMockedStatic.when(() -> HttpUtils.post(anyString(), anyMap(), anyString()))
  99 + .thenReturn("{\"code\":\"E000102\",\"description\":\"Invalid app_key.\"}");
  100 +
  101 + // 调用
  102 + SmsSendRespDTO result = smsClient.sendSms(sendLogId, mobile,
  103 + apiTemplateId, templateParams);
  104 + // 断言
  105 + assertFalse(result.getSuccess());
  106 + assertEquals("E000102", result.getApiCode());
  107 + assertEquals("Invalid app_key.", result.getApiMsg());
  108 + }
  109 + }
  110 +
  111 + @Test
  112 + public void testParseSmsReceiveStatus() {
  113 + // 准备参数
  114 + String text = "sequence=1&total=1&statusDesc=%E7%94%A8%E6%88%B7%E5%B7%B2%E6%88%90%E5%8A%9F%E6%94%B6%E5%88%B0%E7%9F%AD%E4%BF%A1&updateTime=2024-08-15T03%3A00%3A34Z&source=2&smsMsgId=70207ed7-1d02-41b0-8537-bb25fd1c2364_143684459&status=DELIVRD&extend=176";
  115 +
  116 + // 调用
  117 + List<SmsReceiveRespDTO> statuses = smsClient.parseSmsReceiveStatus(text);
  118 + // 断言
  119 + assertEquals(1, statuses.size());
  120 + SmsReceiveRespDTO status = statuses.get(0);
  121 + assertTrue(status.getSuccess());
  122 + assertEquals("DELIVRD", status.getErrorCode());
  123 + assertEquals(LocalDateTime.of(2024, 8, 15, 3, 0, 34), status.getReceiveTime());
  124 + assertEquals("70207ed7-1d02-41b0-8537-bb25fd1c2364_143684459", status.getSerialNo());
  125 + }
  126 +
  127 +}
yudao-module-system/yudao-module-system-biz/src/test/java/cn/iocoder/yudao/module/system/framework/sms/core/client/impl/QiniuSmsClientTest.java 0 → 100644
  1 +package cn.iocoder.yudao.module.system.framework.sms.core.client.impl;
  2 +
  3 +import cn.iocoder.yudao.framework.common.core.KeyValue;
  4 +import cn.iocoder.yudao.framework.common.util.http.HttpUtils;
  5 +import cn.iocoder.yudao.framework.test.core.ut.BaseMockitoUnitTest;
  6 +import cn.iocoder.yudao.module.system.framework.sms.core.client.dto.SmsReceiveRespDTO;
  7 +import cn.iocoder.yudao.module.system.framework.sms.core.client.dto.SmsSendRespDTO;
  8 +import cn.iocoder.yudao.module.system.framework.sms.core.client.dto.SmsTemplateRespDTO;
  9 +import cn.iocoder.yudao.module.system.framework.sms.core.enums.SmsTemplateAuditStatusEnum;
  10 +import cn.iocoder.yudao.module.system.framework.sms.core.property.SmsChannelProperties;
  11 +import com.google.common.collect.Lists;
  12 +
  13 +import org.junit.jupiter.api.Test;
  14 +import org.mockito.InjectMocks;
  15 +import org.mockito.MockedStatic;
  16 +
  17 +import java.time.LocalDateTime;
  18 +import java.util.List;
  19 +
  20 +import static cn.iocoder.yudao.framework.test.core.util.RandomUtils.*;
  21 +import static org.junit.jupiter.api.Assertions.*;
  22 +import static org.mockito.ArgumentMatchers.*;
  23 +import static org.mockito.ArgumentMatchers.anyString;
  24 +import static org.mockito.Mockito.mockStatic;
  25 +
  26 +/**
  27 + * {@link QiniuSmsClient} 的单元测试
  28 + *
  29 + * @author scholar
  30 + */
  31 +public class QiniuSmsClientTest extends BaseMockitoUnitTest {
  32 +
  33 + private final SmsChannelProperties properties = new SmsChannelProperties()
  34 + .setApiKey(randomString())// 随机一个 apiKey,避免构建报错
  35 + .setApiSecret(randomString()) // 随机一个 apiSecret,避免构建报错
  36 + .setSignature("芋道源码");
  37 +
  38 + @InjectMocks
  39 + private QiniuSmsClient smsClient = new QiniuSmsClient(properties);
  40 +
  41 + @Test
  42 + public void testDoSendSms_success() throws Throwable {
  43 + try (MockedStatic<HttpUtils> httpUtilsMockedStatic = mockStatic(HttpUtils.class)) {
  44 + // 准备参数
  45 + Long sendLogId = randomLongId();
  46 + String mobile = randomString();
  47 + String apiTemplateId = randomString() + " " + randomString();
  48 + List<KeyValue<String, Object>> templateParams = Lists.newArrayList(
  49 + new KeyValue<>("1", 1234), new KeyValue<>("2", "login"));
  50 + // mock 方法
  51 + httpUtilsMockedStatic.when(() -> HttpUtils.post(anyString(), anyMap(), anyString()))
  52 + .thenReturn("{\"message_id\":\"17245678901\"}");
  53 + // 调用
  54 + SmsSendRespDTO result = smsClient.sendSms(sendLogId, mobile,
  55 + apiTemplateId, templateParams);
  56 + // 断言
  57 + assertTrue(result.getSuccess());
  58 + assertEquals("17245678901", result.getSerialNo());
  59 + }
  60 + }
  61 +
  62 + @Test
  63 + public void testDoSendSms_fail() throws Throwable {
  64 + try (MockedStatic<HttpUtils> httpUtilsMockedStatic = mockStatic(HttpUtils.class)) {
  65 + // 准备参数
  66 + Long sendLogId = randomLongId();
  67 + String mobile = randomString();
  68 + String apiTemplateId = randomString() + " " + randomString();
  69 + List<KeyValue<String, Object>> templateParams = Lists.newArrayList(
  70 + new KeyValue<>("1", 1234), new KeyValue<>("2", "login"));
  71 + // mock 方法
  72 + httpUtilsMockedStatic.when(() -> HttpUtils.post(anyString(), anyMap(), anyString()))
  73 + .thenReturn("{\"error\":\"BadToken\",\"message\":\"Your authorization token is invalid\",\"request_id\":\"etziWcJFo1C8Ne8X\"}");
  74 + // 调用
  75 + SmsSendRespDTO result = smsClient.sendSms(sendLogId, mobile,
  76 + apiTemplateId, templateParams);
  77 + // 断言
  78 + assertFalse(result.getSuccess());
  79 + assertEquals("BadToken", result.getApiCode());
  80 + assertEquals("Your authorization token is invalid", result.getApiMsg());
  81 + assertEquals("etziWcJFo1C8Ne8X", result.getApiRequestId());
  82 + }
  83 + }
  84 +
  85 + @Test
  86 + public void testGetSmsTemplate() throws Throwable {
  87 + try (MockedStatic<HttpUtils> httpUtilsMockedStatic = mockStatic(HttpUtils.class)) {
  88 + // 准备参数
  89 + String apiTemplateId = randomString();
  90 + // mock 方法
  91 + httpUtilsMockedStatic.when(() -> HttpUtils.get(anyString(), anyMap()))
  92 + .thenReturn("{\"audit_status\":\"passed\",\"created_at\":1724231187,\"description\":\"\",\"disable_broadcast\":false,\"disable_broadcast_reason\":\"\",\"disable_reason\":\"\",\"disabled\":false,\"id\":\"1826184073773596672\",\"is_oversea\":false,\"name\":\"dd\",\"parameters\":[\"code\"],\"reject_reason\":\"\",\"signature_id\":\"1826099896017498112\",\"signature_text\":\"yudao\",\"template\":\"您的验证码为:${code}\",\"type\":\"verification\",\"uid\":1383022432,\"updated_at\":1724288561,\"variable_count\":0}");
  93 + // 调用
  94 + SmsTemplateRespDTO result = smsClient.getSmsTemplate(apiTemplateId);
  95 + // 断言
  96 + assertEquals("1826184073773596672", result.getId());
  97 + assertEquals("您的验证码为:${code}", result.getContent());
  98 + assertEquals(SmsTemplateAuditStatusEnum.SUCCESS.getStatus(), result.getAuditStatus());
  99 + assertEquals("", result.getAuditReason());
  100 + }
  101 + }
  102 +
  103 + @Test
  104 + public void testParseSmsReceiveStatus() {
  105 + // 准备参数
  106 + String text = "{\"items\":[{\"mobile\":\"18881234567\",\"message_id\":\"10135515063508004167\",\"status\":\"DELIVRD\",\"delivrd_at\":1724591666,\"error\":\"DELIVRD\",\"seq\":\"123\"}]}";
  107 + // 调用
  108 + List<SmsReceiveRespDTO> statuses = smsClient.parseSmsReceiveStatus(text);
  109 + // 断言
  110 + assertEquals(1, statuses.size());
  111 + SmsReceiveRespDTO status = statuses.get(0);
  112 + assertTrue(status.getSuccess());
  113 + assertEquals("DELIVRD", status.getErrorMsg());
  114 + assertEquals(LocalDateTime.of(2024, 8, 25, 21, 14, 26), status.getReceiveTime());
  115 + assertEquals("18881234567", status.getMobile());
  116 + assertEquals("10135515063508004167", status.getSerialNo());
  117 + assertEquals(123, status.getLogId());
  118 + }
  119 +
  120 + @Test
  121 + public void testConvertSmsTemplateAuditStatus() {
  122 + assertEquals(SmsTemplateAuditStatusEnum.SUCCESS.getStatus(),
  123 + smsClient.convertSmsTemplateAuditStatus("passed"));
  124 + assertEquals(SmsTemplateAuditStatusEnum.CHECKING.getStatus(),
  125 + smsClient.convertSmsTemplateAuditStatus("reviewing"));
  126 + assertEquals(SmsTemplateAuditStatusEnum.FAIL.getStatus(),
  127 + smsClient.convertSmsTemplateAuditStatus("rejected"));
  128 + assertThrows(IllegalArgumentException.class, () -> smsClient.convertSmsTemplateAuditStatus("unknown"),
  129 + "未知审核状态(3)");
  130 + }
  131 +}
0 \ No newline at end of file 132 \ No newline at end of file
yudao-module-system/yudao-module-system-biz/src/test/java/cn/iocoder/yudao/module/system/framework/sms/core/client/impl/SmsClientTests.java
@@ -2,7 +2,7 @@ package cn.iocoder.yudao.module.system.framework.sms.core.client.impl; @@ -2,7 +2,7 @@ package cn.iocoder.yudao.module.system.framework.sms.core.client.impl;
2 2
3 import cn.hutool.core.collection.ListUtil; 3 import cn.hutool.core.collection.ListUtil;
4 import cn.iocoder.yudao.framework.common.core.KeyValue; 4 import cn.iocoder.yudao.framework.common.core.KeyValue;
5 -import cn.iocoder.yudao.module.system.framework.sms.core.client.dto.SmsReceiveRespDTO; 5 +import cn.iocoder.yudao.module.system.framework.sms.core.client.SmsClient;
6 import cn.iocoder.yudao.module.system.framework.sms.core.client.dto.SmsSendRespDTO; 6 import cn.iocoder.yudao.module.system.framework.sms.core.client.dto.SmsSendRespDTO;
7 import cn.iocoder.yudao.module.system.framework.sms.core.client.dto.SmsTemplateRespDTO; 7 import cn.iocoder.yudao.module.system.framework.sms.core.client.dto.SmsTemplateRespDTO;
8 import cn.iocoder.yudao.module.system.framework.sms.core.property.SmsChannelProperties; 8 import cn.iocoder.yudao.module.system.framework.sms.core.property.SmsChannelProperties;
@@ -12,7 +12,7 @@ import org.junit.jupiter.api.Test; @@ -12,7 +12,7 @@ import org.junit.jupiter.api.Test;
12 import java.util.List; 12 import java.util.List;
13 13
14 /** 14 /**
15 - * 各种 {@link SmsClientTests 集成测试 15 + * 各种 {@link SmsClient} 的集成测试
16 * 16 *
17 * @author 芋道源码 17 * @author 芋道源码
18 */ 18 */
@@ -24,8 +24,8 @@ public class SmsClientTests { @@ -24,8 +24,8 @@ public class SmsClientTests {
24 @Disabled 24 @Disabled
25 public void testAliyunSmsClient_getSmsTemplate() throws Throwable { 25 public void testAliyunSmsClient_getSmsTemplate() throws Throwable {
26 SmsChannelProperties properties = new SmsChannelProperties() 26 SmsChannelProperties properties = new SmsChannelProperties()
27 - .setApiKey("LTAI5tAicJAxaSFiZuGGeXHR")  
28 - .setApiSecret("Fdr9vadxnDvS6GJU0W1tijQ0VmLhYz"); 27 + .setApiKey(System.getenv("SMS_ALIYUN_ACCESS_KEY"))
  28 + .setApiSecret(System.getenv("SMS_ALIYUN_SECRET_KEY"));
29 AliyunSmsClient client = new AliyunSmsClient(properties); 29 AliyunSmsClient client = new AliyunSmsClient(properties);
30 // 准备参数 30 // 准备参数
31 String apiTemplateId = "SMS_207945135"; 31 String apiTemplateId = "SMS_207945135";
@@ -39,9 +39,9 @@ public class SmsClientTests { @@ -39,9 +39,9 @@ public class SmsClientTests {
39 @Disabled 39 @Disabled
40 public void testAliyunSmsClient_sendSms() throws Throwable { 40 public void testAliyunSmsClient_sendSms() throws Throwable {
41 SmsChannelProperties properties = new SmsChannelProperties() 41 SmsChannelProperties properties = new SmsChannelProperties()
42 - .setApiKey("LTAI5tAicJAxaSFiZuGGeXHR")  
43 - .setApiSecret("Fdr9vadxnDvS6GJU0W1tijQ0VmLhYz")  
44 - .setSignature("runpu"); 42 + .setApiKey(System.getenv("SMS_ALIYUN_ACCESS_KEY"))
  43 + .setApiSecret(System.getenv("SMS_ALIYUN_SECRET_KEY"))
  44 + .setSignature("Ballcat");
45 AliyunSmsClient client = new AliyunSmsClient(properties); 45 AliyunSmsClient client = new AliyunSmsClient(properties);
46 // 准备参数 46 // 准备参数
47 Long sendLogId = System.currentTimeMillis(); 47 Long sendLogId = System.currentTimeMillis();
@@ -53,49 +53,21 @@ public class SmsClientTests { @@ -53,49 +53,21 @@ public class SmsClientTests {
53 System.out.println(sendRespDTO); 53 System.out.println(sendRespDTO);
54 } 54 }
55 55
56 - @Test  
57 - @Disabled  
58 - public void testAliyunSmsClient_parseSmsReceiveStatus() {  
59 - SmsChannelProperties properties = new SmsChannelProperties()  
60 - .setApiKey("LTAI5tAicJAxaSFiZuGGeXHR")  
61 - .setApiSecret("Fdr9vadxnDvS6GJU0W1tijQ0VmLhYz");  
62 - AliyunSmsClient client = new AliyunSmsClient(properties);  
63 - // 准备参数  
64 - String text = "[\n" +  
65 - " {\n" +  
66 - " \"phone_number\" : \"13900000001\",\n" +  
67 - " \"send_time\" : \"2017-01-01 11:12:13\",\n" +  
68 - " \"report_time\" : \"2017-02-02 22:23:24\",\n" +  
69 - " \"success\" : true,\n" +  
70 - " \"err_code\" : \"DELIVERED\",\n" +  
71 - " \"err_msg\" : \"用户接收成功\",\n" +  
72 - " \"sms_size\" : \"1\",\n" +  
73 - " \"biz_id\" : \"12345\",\n" +  
74 - " \"out_id\" : \"67890\"\n" +  
75 - " }\n" +  
76 - "]";  
77 - // mock 方法  
78 -  
79 - // 调用  
80 - List<SmsReceiveRespDTO> statuses = client.parseSmsReceiveStatus(text);  
81 - // 打印结果  
82 - System.out.println(statuses);  
83 - }  
84 -  
85 // ========== 腾讯云 ========== 56 // ========== 腾讯云 ==========
86 57
87 @Test 58 @Test
88 @Disabled 59 @Disabled
89 public void testTencentSmsClient_sendSms() throws Throwable { 60 public void testTencentSmsClient_sendSms() throws Throwable {
  61 + String sdkAppId = "1400500458";
90 SmsChannelProperties properties = new SmsChannelProperties() 62 SmsChannelProperties properties = new SmsChannelProperties()
91 - .setApiKey("LTAI5tAicJAxaSFiZuGGeXHR 1428926523")  
92 - .setApiSecret("Fdr9vadxnDvS6GJU0W1tijQ0VmLhYz") 63 + .setApiKey(System.getenv("SMS_TENCENT_ACCESS_KEY") + " " + sdkAppId)
  64 + .setApiSecret(System.getenv("SMS_TENCENT_SECRET_KEY"))
93 .setSignature("芋道源码"); 65 .setSignature("芋道源码");
94 TencentSmsClient client = new TencentSmsClient(properties); 66 TencentSmsClient client = new TencentSmsClient(properties);
95 // 准备参数 67 // 准备参数
96 Long sendLogId = System.currentTimeMillis(); 68 Long sendLogId = System.currentTimeMillis();
97 String mobile = "15601691323"; 69 String mobile = "15601691323";
98 - String apiTemplateId = "2136358"; 70 + String apiTemplateId = "358212";
99 // 调用 71 // 调用
100 SmsSendRespDTO sendRespDTO = client.sendSms(sendLogId, mobile, apiTemplateId, ListUtil.of(new KeyValue<>("code", "1024"))); 72 SmsSendRespDTO sendRespDTO = client.sendSms(sendLogId, mobile, apiTemplateId, ListUtil.of(new KeyValue<>("code", "1024")));
101 // 打印结果 73 // 打印结果
@@ -105,13 +77,14 @@ public class SmsClientTests { @@ -105,13 +77,14 @@ public class SmsClientTests {
105 @Test 77 @Test
106 @Disabled 78 @Disabled
107 public void testTencentSmsClient_getSmsTemplate() throws Throwable { 79 public void testTencentSmsClient_getSmsTemplate() throws Throwable {
  80 + String sdkAppId = "1400500458";
108 SmsChannelProperties properties = new SmsChannelProperties() 81 SmsChannelProperties properties = new SmsChannelProperties()
109 - .setApiKey("LTAI5tAicJAxaSFiZuGGeXHR 1428926523")  
110 - .setApiSecret("Fdr9vadxnDvS6GJU0W1tijQ0VmLhYz") 82 + .setApiKey(System.getenv("SMS_TENCENT_ACCESS_KEY") + " " + sdkAppId)
  83 + .setApiSecret(System.getenv("SMS_TENCENT_SECRET_KEY"))
111 .setSignature("芋道源码"); 84 .setSignature("芋道源码");
112 TencentSmsClient client = new TencentSmsClient(properties); 85 TencentSmsClient client = new TencentSmsClient(properties);
113 // 准备参数 86 // 准备参数
114 - String apiTemplateId = "2136358"; 87 + String apiTemplateId = "358212";
115 // 调用 88 // 调用
116 SmsTemplateRespDTO template = client.getSmsTemplate(apiTemplateId); 89 SmsTemplateRespDTO template = client.getSmsTemplate(apiTemplateId);
117 // 打印结果 90 // 打印结果
@@ -123,15 +96,16 @@ public class SmsClientTests { @@ -123,15 +96,16 @@ public class SmsClientTests {
123 @Test 96 @Test
124 @Disabled 97 @Disabled
125 public void testHuaweiSmsClient_sendSms() throws Throwable { 98 public void testHuaweiSmsClient_sendSms() throws Throwable {
  99 + String sender = "x8824060312575";
126 SmsChannelProperties properties = new SmsChannelProperties() 100 SmsChannelProperties properties = new SmsChannelProperties()
127 - .setApiKey("123")  
128 - .setApiSecret("456") 101 + .setApiKey(System.getenv("SMS_HUAWEI_ACCESS_KEY") + " " + sender)
  102 + .setApiSecret(System.getenv("SMS_HUAWEI_SECRET_KEY"))
129 .setSignature("runpu"); 103 .setSignature("runpu");
130 HuaweiSmsClient client = new HuaweiSmsClient(properties); 104 HuaweiSmsClient client = new HuaweiSmsClient(properties);
131 // 准备参数 105 // 准备参数
132 Long sendLogId = System.currentTimeMillis(); 106 Long sendLogId = System.currentTimeMillis();
133 - String mobile = "15601691323";  
134 - String apiTemplateId = "xx test01"; 107 + String mobile = "17321315478";
  108 + String apiTemplateId = "3644cdab863546a3b718d488659a99ef";
135 List<KeyValue<String, Object>> templateParams = ListUtil.of(new KeyValue<>("code", "1024")); 109 List<KeyValue<String, Object>> templateParams = ListUtil.of(new KeyValue<>("code", "1024"));
136 // 调用 110 // 调用
137 SmsSendRespDTO smsSendRespDTO = client.sendSms(sendLogId, mobile, apiTemplateId, templateParams); 111 SmsSendRespDTO smsSendRespDTO = client.sendSms(sendLogId, mobile, apiTemplateId, templateParams);
@@ -139,5 +113,39 @@ public class SmsClientTests { @@ -139,5 +113,39 @@ public class SmsClientTests {
139 System.out.println(smsSendRespDTO); 113 System.out.println(smsSendRespDTO);
140 } 114 }
141 115
  116 + // ========== 七牛云 ==========
  117 +
  118 + @Test
  119 + @Disabled
  120 + public void testQiniuSmsClient_sendSms() throws Throwable {
  121 + SmsChannelProperties properties = new SmsChannelProperties()
  122 + .setApiKey("SMS_QINIU_ACCESS_KEY")
  123 + .setApiSecret("SMS_QINIU_SECRET_KEY");
  124 + QiniuSmsClient client = new QiniuSmsClient(properties);
  125 + // 准备参数
  126 + Long sendLogId = System.currentTimeMillis();
  127 + String mobile = "17321315478";
  128 + String apiTemplateId = "3644cdab863546a3b718d488659a99ef";
  129 + List<KeyValue<String, Object>> templateParams = ListUtil.of(new KeyValue<>("code", "1122"));
  130 + // 调用
  131 + SmsSendRespDTO smsSendRespDTO = client.sendSms(sendLogId, mobile, apiTemplateId, templateParams);
  132 + // 打印结果
  133 + System.out.println(smsSendRespDTO);
  134 + }
  135 +
  136 + @Test
  137 + @Disabled
  138 + public void testQiniuSmsClient_getSmsTemplate() throws Throwable {
  139 + SmsChannelProperties properties = new SmsChannelProperties()
  140 + .setApiKey("SMS_QINIU_ACCESS_KEY")
  141 + .setApiSecret("SMS_QINIU_SECRET_KEY");
  142 + QiniuSmsClient client = new QiniuSmsClient(properties);
  143 + // 准备参数
  144 + String apiTemplateId = "3644cdab863546a3b718d488659a99ef";
  145 + // 调用
  146 + SmsTemplateRespDTO template = client.getSmsTemplate(apiTemplateId);
  147 + // 打印结果
  148 + System.out.println(template);
  149 + }
142 } 150 }
143 151
yudao-module-system/yudao-module-system-biz/src/test/java/cn/iocoder/yudao/module/system/framework/sms/core/client/impl/TencentSmsClientTest.java
@@ -78,7 +78,7 @@ public class TencentSmsClientTest extends BaseMockitoUnitTest { @@ -78,7 +78,7 @@ public class TencentSmsClientTest extends BaseMockitoUnitTest {
78 } 78 }
79 79
80 @Test 80 @Test
81 - public void testDoSendSms_fail() throws Throwable { 81 + public void testDoSendSms_fail_01() throws Throwable {
82 try (MockedStatic<HttpUtils> httpUtilsMockedStatic = mockStatic(HttpUtils.class)) { 82 try (MockedStatic<HttpUtils> httpUtilsMockedStatic = mockStatic(HttpUtils.class)) {
83 // 准备参数 83 // 准备参数
84 Long sendLogId = randomLongId(); 84 Long sendLogId = randomLongId();
@@ -118,6 +118,31 @@ public class TencentSmsClientTest extends BaseMockitoUnitTest { @@ -118,6 +118,31 @@ public class TencentSmsClientTest extends BaseMockitoUnitTest {
118 } 118 }
119 119
120 @Test 120 @Test
  121 + public void testDoSendSms_fail_02() throws Throwable {
  122 + try (MockedStatic<HttpUtils> httpUtilsMockedStatic = mockStatic(HttpUtils.class)) {
  123 + // 准备参数
  124 + Long sendLogId = randomLongId();
  125 + String mobile = randomString();
  126 + String apiTemplateId = randomString();
  127 + List<KeyValue<String, Object>> templateParams = Lists.newArrayList(
  128 + new KeyValue<>("1", 1234), new KeyValue<>("2", "login"));
  129 +
  130 + // mock 方法
  131 + httpUtilsMockedStatic.when(() -> HttpUtils.post(anyString(), anyMap(), anyString()))
  132 + .thenReturn("{\"Response\":{\"Error\":{\"Code\":\"AuthFailure.SecretIdNotFound\",\"Message\":\"The SecretId is not found, please ensure that your SecretId is correct.\"},\"RequestId\":\"2a88f82a-261c-4ac6-9fa9-c7d01aaa486a\"}}");
  133 +
  134 + // 调用
  135 + SmsSendRespDTO result = smsClient.sendSms(sendLogId, mobile,
  136 + apiTemplateId, templateParams);
  137 + // 断言
  138 + assertFalse(result.getSuccess());
  139 + assertEquals("2a88f82a-261c-4ac6-9fa9-c7d01aaa486a", result.getApiRequestId());
  140 + assertEquals("AuthFailure.SecretIdNotFound", result.getApiCode());
  141 + assertEquals("The SecretId is not found, please ensure that your SecretId is correct.", result.getApiMsg());
  142 + }
  143 + }
  144 +
  145 + @Test
121 public void testParseSmsReceiveStatus() { 146 public void testParseSmsReceiveStatus() {
122 // 准备参数 147 // 准备参数
123 String text = "[\n" + 148 String text = "[\n" +
yudao-module-system/yudao-module-system-biz/src/test/java/cn/iocoder/yudao/module/system/service/sms/SmsChannelServiceTest.java
@@ -57,9 +57,6 @@ public class SmsChannelServiceTest extends BaseDbUnitTest { @@ -57,9 +57,6 @@ public class SmsChannelServiceTest extends BaseDbUnitTest {
57 // 校验记录的属性是否正确 57 // 校验记录的属性是否正确
58 SmsChannelDO smsChannel = smsChannelMapper.selectById(smsChannelId); 58 SmsChannelDO smsChannel = smsChannelMapper.selectById(smsChannelId);
59 assertPojoEquals(reqVO, smsChannel, "id"); 59 assertPojoEquals(reqVO, smsChannel, "id");
60 - // 断言 cache  
61 - assertNull(smsChannelService.getIdClientCache().getIfPresent(smsChannel.getId()));  
62 - assertNull(smsChannelService.getCodeClientCache().getIfPresent(smsChannel.getCode()));  
63 } 60 }
64 61
65 @Test 62 @Test
@@ -79,9 +76,6 @@ public class SmsChannelServiceTest extends BaseDbUnitTest { @@ -79,9 +76,6 @@ public class SmsChannelServiceTest extends BaseDbUnitTest {
79 // 校验是否更新正确 76 // 校验是否更新正确
80 SmsChannelDO smsChannel = smsChannelMapper.selectById(reqVO.getId()); // 获取最新的 77 SmsChannelDO smsChannel = smsChannelMapper.selectById(reqVO.getId()); // 获取最新的
81 assertPojoEquals(reqVO, smsChannel); 78 assertPojoEquals(reqVO, smsChannel);
82 - // 断言 cache  
83 - assertNull(smsChannelService.getIdClientCache().getIfPresent(smsChannel.getId()));  
84 - assertNull(smsChannelService.getCodeClientCache().getIfPresent(smsChannel.getCode()));  
85 } 79 }
86 80
87 @Test 81 @Test
@@ -105,9 +99,6 @@ public class SmsChannelServiceTest extends BaseDbUnitTest { @@ -105,9 +99,6 @@ public class SmsChannelServiceTest extends BaseDbUnitTest {
105 smsChannelService.deleteSmsChannel(id); 99 smsChannelService.deleteSmsChannel(id);
106 // 校验数据不存在了 100 // 校验数据不存在了
107 assertNull(smsChannelMapper.selectById(id)); 101 assertNull(smsChannelMapper.selectById(id));
108 - // 断言 cache  
109 - assertNull(smsChannelService.getIdClientCache().getIfPresent(dbSmsChannel.getId()));  
110 - assertNull(smsChannelService.getCodeClientCache().getIfPresent(dbSmsChannel.getCode()));  
111 } 102 }
112 103
113 @Test 104 @Test
@@ -196,29 +187,23 @@ public class SmsChannelServiceTest extends BaseDbUnitTest { @@ -196,29 +187,23 @@ public class SmsChannelServiceTest extends BaseDbUnitTest {
196 // mock 数据 187 // mock 数据
197 SmsChannelDO channel = randomPojo(SmsChannelDO.class); 188 SmsChannelDO channel = randomPojo(SmsChannelDO.class);
198 smsChannelMapper.insert(channel); 189 smsChannelMapper.insert(channel);
199 - // mock 参数 190 + // 准备参数
200 Long id = channel.getId(); 191 Long id = channel.getId();
201 // mock 方法 192 // mock 方法
202 SmsClient mockClient = mock(SmsClient.class); 193 SmsClient mockClient = mock(SmsClient.class);
203 - when(smsClientFactory.getSmsClient(eq(id))).thenReturn(mockClient); 194 + SmsChannelProperties properties = BeanUtils.toBean(channel, SmsChannelProperties.class);
  195 + when(smsClientFactory.createOrUpdateSmsClient(eq(properties))).thenReturn(mockClient);
204 196
205 // 调用 197 // 调用
206 SmsClient client = smsChannelService.getSmsClient(id); 198 SmsClient client = smsChannelService.getSmsClient(id);
207 // 断言 199 // 断言
208 assertSame(client, mockClient); 200 assertSame(client, mockClient);
209 - verify(smsClientFactory).createOrUpdateSmsClient(argThat(arg -> {  
210 - SmsChannelProperties properties = BeanUtils.toBean(channel, SmsChannelProperties.class);  
211 - return properties.equals(arg);  
212 - }));  
213 } 201 }
214 202
215 @Test 203 @Test
216 public void testGetSmsClient_code() { 204 public void testGetSmsClient_code() {
217 - // mock 数据  
218 - SmsChannelDO channel = randomPojo(SmsChannelDO.class);  
219 - smsChannelMapper.insert(channel);  
220 - // mock 参数  
221 - String code = channel.getCode(); 205 + // 准备参数
  206 + String code = randomString();
222 // mock 方法 207 // mock 方法
223 SmsClient mockClient = mock(SmsClient.class); 208 SmsClient mockClient = mock(SmsClient.class);
224 when(smsClientFactory.getSmsClient(eq(code))).thenReturn(mockClient); 209 when(smsClientFactory.getSmsClient(eq(code))).thenReturn(mockClient);
@@ -227,10 +212,6 @@ public class SmsChannelServiceTest extends BaseDbUnitTest { @@ -227,10 +212,6 @@ public class SmsChannelServiceTest extends BaseDbUnitTest {
227 SmsClient client = smsChannelService.getSmsClient(code); 212 SmsClient client = smsChannelService.getSmsClient(code);
228 // 断言 213 // 断言
229 assertSame(client, mockClient); 214 assertSame(client, mockClient);
230 - verify(smsClientFactory).createOrUpdateSmsClient(argThat(arg -> {  
231 - SmsChannelProperties properties = BeanUtils.toBean(channel, SmsChannelProperties.class);  
232 - return properties.equals(arg);  
233 - }));  
234 } 215 }
235 216
236 } 217 }
yudao-server/pom.xml
@@ -33,11 +33,11 @@ @@ -33,11 +33,11 @@
33 </dependency> 33 </dependency>
34 34
35 <!-- 会员中心。默认注释,保证编译速度 --> 35 <!-- 会员中心。默认注释,保证编译速度 -->
36 -<!-- <dependency>-->  
37 -<!-- <groupId>cn.iocoder.boot</groupId>-->  
38 -<!-- <artifactId>yudao-module-member-biz</artifactId>-->  
39 -<!-- <version>${revision}</version>-->  
40 -<!-- </dependency>--> 36 + <dependency>
  37 + <groupId>cn.iocoder.boot</groupId>
  38 + <artifactId>yudao-module-member-biz</artifactId>
  39 + <version>${revision}</version>
  40 + </dependency>
41 41
42 <!-- 数据报表。默认注释,保证编译速度 --> 42 <!-- 数据报表。默认注释,保证编译速度 -->
43 <!-- <dependency>--> 43 <!-- <dependency>-->
@@ -52,11 +52,11 @@ @@ -52,11 +52,11 @@
52 <!-- <version>${revision}</version>--> 52 <!-- <version>${revision}</version>-->
53 <!-- </dependency>--> 53 <!-- </dependency>-->
54 <!-- 支付服务。默认注释,保证编译速度 --> 54 <!-- 支付服务。默认注释,保证编译速度 -->
55 -<!-- <dependency>-->  
56 -<!-- <groupId>cn.iocoder.boot</groupId>-->  
57 -<!-- <artifactId>yudao-module-pay-biz</artifactId>-->  
58 -<!-- <version>${revision}</version>-->  
59 -<!-- </dependency>--> 55 + <dependency>
  56 + <groupId>cn.iocoder.boot</groupId>
  57 + <artifactId>yudao-module-pay-biz</artifactId>
  58 + <version>${revision}</version>
  59 + </dependency>
60 60
61 <!-- 微信公众号模块。默认注释,保证编译速度 --> 61 <!-- 微信公众号模块。默认注释,保证编译速度 -->
62 <!-- <dependency>--> 62 <!-- <dependency>-->
@@ -66,26 +66,26 @@ @@ -66,26 +66,26 @@
66 <!-- </dependency>--> 66 <!-- </dependency>-->
67 67
68 <!-- 商城相关模块。默认注释,保证编译速度--> 68 <!-- 商城相关模块。默认注释,保证编译速度-->
69 -<!-- <dependency>-->  
70 -<!-- <groupId>cn.iocoder.boot</groupId>-->  
71 -<!-- <artifactId>yudao-module-promotion-biz</artifactId>-->  
72 -<!-- <version>${revision}</version>-->  
73 -<!-- </dependency>-->  
74 -<!-- <dependency>-->  
75 -<!-- <groupId>cn.iocoder.boot</groupId>-->  
76 -<!-- <artifactId>yudao-module-product-biz</artifactId>-->  
77 -<!-- <version>${revision}</version>-->  
78 -<!-- </dependency>-->  
79 -<!-- <dependency>-->  
80 -<!-- <groupId>cn.iocoder.boot</groupId>-->  
81 -<!-- <artifactId>yudao-module-trade-biz</artifactId>-->  
82 -<!-- <version>${revision}</version>-->  
83 -<!-- </dependency>-->  
84 -<!-- <dependency>-->  
85 -<!-- <groupId>cn.iocoder.boot</groupId>-->  
86 -<!-- <artifactId>yudao-module-statistics-biz</artifactId>-->  
87 -<!-- <version>${revision}</version>-->  
88 -<!-- </dependency>--> 69 + <dependency>
  70 + <groupId>cn.iocoder.boot</groupId>
  71 + <artifactId>yudao-module-promotion-biz</artifactId>
  72 + <version>${revision}</version>
  73 + </dependency>
  74 + <dependency>
  75 + <groupId>cn.iocoder.boot</groupId>
  76 + <artifactId>yudao-module-product-biz</artifactId>
  77 + <version>${revision}</version>
  78 + </dependency>
  79 + <dependency>
  80 + <groupId>cn.iocoder.boot</groupId>
  81 + <artifactId>yudao-module-trade-biz</artifactId>
  82 + <version>${revision}</version>
  83 + </dependency>
  84 + <dependency>
  85 + <groupId>cn.iocoder.boot</groupId>
  86 + <artifactId>yudao-module-statistics-biz</artifactId>
  87 + <version>${revision}</version>
  88 + </dependency>
89 89
90 <!-- CRM 相关模块。默认注释,保证编译速度 --> 90 <!-- CRM 相关模块。默认注释,保证编译速度 -->
91 <!-- <dependency>--> 91 <!-- <dependency>-->
yudao-server/src/main/resources/application-dev.yaml
@@ -40,12 +40,12 @@ spring: @@ -40,12 +40,12 @@ spring:
40 primary: master 40 primary: master
41 datasource: 41 datasource:
42 master: 42 master:
43 - url: jdbc:mysql://127.0.0.1:3306/ruoyi-vue-pro?useSSL=false&serverTimezone=Asia/Shanghai&allowPublicKeyRetrieval=true&nullCatalogMeansCurrent=true # MySQL Connector/J 8.X 连接的示例 43 + url: jdbc:mysql://127.0.0.1:3306/ruoyi-vue-pro?useSSL=false&serverTimezone=Asia/Shanghai&allowPublicKeyRetrieval=true&nullCatalogMeansCurrent=true&rewriteBatchedStatements=true # MySQL Connector/J 8.X 连接的示例
44 username: root 44 username: root
45 password: 123456 45 password: 123456
46 slave: # 模拟从库,可根据自己需要修改 # 模拟从库,可根据自己需要修改 46 slave: # 模拟从库,可根据自己需要修改 # 模拟从库,可根据自己需要修改
47 lazy: true # 开启懒加载,保证启动速度 47 lazy: true # 开启懒加载,保证启动速度
48 - url: jdbc:mysql://127.0.0.1:3306/ruoyi-vue-pro?useSSL=false&serverTimezone=Asia/Shanghai&allowPublicKeyRetrieval=true&nullCatalogMeansCurrent=true # MySQL Connector/J 8.X 连接的示例 48 + url: jdbc:mysql://127.0.0.1:3306/ruoyi-vue-pro?useSSL=false&serverTimezone=Asia/Shanghai&allowPublicKeyRetrieval=true&nullCatalogMeansCurrent=true&rewriteBatchedStatements=true # MySQL Connector/J 8.X 连接的示例
49 username: root 49 username: root
50 password: 123456 50 password: 123456
51 51
yudao-server/src/main/resources/application-local.yaml
@@ -45,8 +45,8 @@ spring: @@ -45,8 +45,8 @@ spring:
45 primary: master 45 primary: master
46 datasource: 46 datasource:
47 master: 47 master:
48 - url: jdbc:mysql://127.0.0.1:3306/ruoyi-vue-pro?useSSL=false&serverTimezone=Asia/Shanghai&allowPublicKeyRetrieval=true&nullCatalogMeansCurrent=true # MySQL Connector/J 8.X 连接的示例  
49 - # url: jdbc:mysql://127.0.0.1:3306/ruoyi-vue-pro?useSSL=true&allowPublicKeyRetrieval=true&useUnicode=true&characterEncoding=UTF-8&serverTimezone=Asia/Shanghai # MySQL Connector/J 5.X 连接的示例 48 + url: jdbc:mysql://127.0.0.1:3306/ruoyi-vue-pro?useSSL=false&serverTimezone=Asia/Shanghai&allowPublicKeyRetrieval=true&nullCatalogMeansCurrent=true&rewriteBatchedStatements=true # MySQL Connector/J 8.X 连接的示例
  49 + # url: jdbc:mysql://127.0.0.1:3306/ruoyi-vue-pro?useSSL=true&allowPublicKeyRetrieval=true&useUnicode=true&characterEncoding=UTF-8&serverTimezone=Asia/Shanghai&rewriteBatchedStatements=true # MySQL Connector/J 5.X 连接的示例
50 # url: jdbc:postgresql://127.0.0.1:5432/ruoyi-vue-pro # PostgreSQL 连接的示例 50 # url: jdbc:postgresql://127.0.0.1:5432/ruoyi-vue-pro # PostgreSQL 连接的示例
51 # url: jdbc:oracle:thin:@127.0.0.1:1521:xe # Oracle 连接的示例 51 # url: jdbc:oracle:thin:@127.0.0.1:1521:xe # Oracle 连接的示例
52 # url: jdbc:sqlserver://127.0.0.1:1433;DatabaseName=ruoyi-vue-pro;SelectMethod=cursor;encrypt=false;rewriteBatchedStatements=true;useUnicode=true;characterEncoding=utf-8 # SQLServer 连接的示例 52 # url: jdbc:sqlserver://127.0.0.1:1433;DatabaseName=ruoyi-vue-pro;SelectMethod=cursor;encrypt=false;rewriteBatchedStatements=true;useUnicode=true;characterEncoding=utf-8 # SQLServer 连接的示例
@@ -63,7 +63,7 @@ spring: @@ -63,7 +63,7 @@ spring:
63 # password: Yudao@2024 # OpenGauss 连接的示例 63 # password: Yudao@2024 # OpenGauss 连接的示例
64 slave: # 模拟从库,可根据自己需要修改 64 slave: # 模拟从库,可根据自己需要修改
65 lazy: true # 开启懒加载,保证启动速度 65 lazy: true # 开启懒加载,保证启动速度
66 - url: jdbc:mysql://127.0.0.1:3306/ruoyi-vue-pro?useSSL=false&serverTimezone=Asia/Shanghai&allowPublicKeyRetrieval=true&nullCatalogMeansCurrent=true 66 + url: jdbc:mysql://127.0.0.1:3306/ruoyi-vue-pro?useSSL=false&serverTimezone=Asia/Shanghai&allowPublicKeyRetrieval=true&rewriteBatchedStatements=true&nullCatalogMeansCurrent=true
67 username: root 67 username: root
68 password: 123456 68 password: 123456
69 69
yudao-server/src/main/resources/application.yaml
@@ -301,6 +301,7 @@ yudao: @@ -301,6 +301,7 @@ yudao:
301 - tmp_report_data_1 301 - tmp_report_data_1
302 - tmp_report_data_income 302 - tmp_report_data_income
303 ignore-caches: 303 ignore-caches:
  304 + - user_role_ids
304 - permission_menu_ids 305 - permission_menu_ids
305 - oauth_client 306 - oauth_client
306 - notify_template 307 - notify_template