Commit 30fd7996f6f2d1ed630a9c72460556ab34cb7bfc
Merge branch 'develop' of https://gitee.com/zhijiantianya/ruoyi-vue-pro into master-jdk17
Showing
71 changed files
with
1622 additions
and
916 deletions
pom.xml
| @@ -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<S3FileClientConfig> { | @@ -91,7 +91,7 @@ public class S3FileClient extends AbstractFileClient<S3FileClientConfig> { | ||
| 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<RewardActivityDO> { | @@ -30,10 +30,6 @@ public interface RewardActivityMapper extends BaseMapperX<RewardActivityDO> { | ||
| 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<RewardActivityDO> { | @@ -53,16 +49,16 @@ public interface RewardActivityMapper extends BaseMapperX<RewardActivityDO> { | ||
| 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 "trade_order" | @@ -48,6 +48,7 @@ CREATE TABLE IF NOT EXISTS "trade_order" | ||
| 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 |