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 | 15 | <!-- 各种 module 拓展 --> |
| 16 | 16 | <module>yudao-module-system</module> |
| 17 | 17 | <module>yudao-module-infra</module> |
| 18 | -<!-- <module>yudao-module-member</module>--> | |
| 18 | + <module>yudao-module-member</module> | |
| 19 | 19 | <!-- <module>yudao-module-bpm</module>--> |
| 20 | 20 | <!-- <module>yudao-module-report</module>--> |
| 21 | 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 | 24 | <!-- <module>yudao-module-crm</module>--> |
| 25 | 25 | <!-- <module>yudao-module-erp</module>--> |
| 26 | 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 | 111 | authorization = Base64.decodeStr(authorization); |
| 112 | 112 | clientId = StrUtil.subBefore(authorization, ":", false); |
| 113 | 113 | clientSecret = StrUtil.subAfter(authorization, ":", false); |
| 114 | - // 再从 Param 中获取 | |
| 114 | + // 再从 Param 中获取 | |
| 115 | 115 | } else { |
| 116 | 116 | clientId = request.getParameter("client_id"); |
| 117 | 117 | clientSecret = request.getParameter("client_secret"); |
| ... | ... | @@ -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 | 91 | * 开启 VirtualStyle 模式 |
| 92 | 92 | */ |
| 93 | 93 | private void enableVirtualStyleEndpoint() { |
| 94 | - if (StrUtil.containsAll(config.getEndpoint(), | |
| 94 | + if (StrUtil.containsAny(config.getEndpoint(), | |
| 95 | 95 | S3FileClientConfig.ENDPOINT_TENCENT, // 腾讯云 https://cloud.tencent.com/document/product/436/41284 |
| 96 | 96 | S3FileClientConfig.ENDPOINT_VOLCES)) { // 火山云 https://www.volcengine.com/docs/6349/1288493 |
| 97 | 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 | 286 | // 校验存在 |
| 287 | 287 | validate${subSimpleClassName}Exists(${subClassNameVar}.getId()); |
| 288 | 288 | // 更新 |
| 289 | + ${subClassNameVar}.setUpdater(null).setUpdateTime(null); // 解决更新情况下:updateTime 不更新 | |
| 289 | 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 | 64 | <el-checkbox |
| 65 | 65 | v-for="dict in $dictMethod(DICT_TYPE.$dictType.toUpperCase())" |
| 66 | 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 | 70 | #else##没数据字典 |
| 72 | - <el-checkbox>请选择字典生成</el-checkbox> | |
| 71 | + <el-checkbox label="请选择字典生成" /> | |
| 73 | 72 | #end |
| 74 | 73 | </el-checkbox-group> |
| 75 | 74 | </el-form-item> |
| ... | ... | @@ -85,7 +84,7 @@ |
| 85 | 84 | {{ dict.label }} |
| 86 | 85 | </el-radio> |
| 87 | 86 | #else##没数据字典 |
| 88 | - <el-radio label="1">请选择字典生成</el-radio> | |
| 87 | + <el-radio value="1">请选择字典生成</el-radio> | |
| 89 | 88 | #end |
| 90 | 89 | </el-radio-group> |
| 91 | 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 | 92 | <el-checkbox |
| 93 | 93 | v-for="dict in $dictMethod(DICT_TYPE.$dictType.toUpperCase())" |
| 94 | 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 | 98 | #else##没数据字典 |
| 100 | - <el-checkbox>请选择字典生成</el-checkbox> | |
| 99 | + <el-checkbox label="请选择字典生成" /> | |
| 101 | 100 | #end |
| 102 | 101 | </el-checkbox-group> |
| 103 | 102 | </el-form-item> |
| ... | ... | @@ -117,7 +116,7 @@ |
| 117 | 116 | {{ dict.label }} |
| 118 | 117 | </el-radio> |
| 119 | 118 | #else##没数据字典 |
| 120 | - <el-radio label="1">请选择字典生成</el-radio> | |
| 119 | + <el-radio value="1">请选择字典生成</el-radio> | |
| 121 | 120 | #end |
| 122 | 121 | </el-radio-group> |
| 123 | 122 | </el-form-item> |
| ... | ... | @@ -219,12 +218,11 @@ |
| 219 | 218 | <el-checkbox |
| 220 | 219 | v-for="dict in $dictMethod(DICT_TYPE.$dictType.toUpperCase())" |
| 221 | 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 | 224 | #else##没数据字典 |
| 227 | - <el-checkbox>请选择字典生成</el-checkbox> | |
| 225 | + <el-checkbox label="请选择字典生成" /> | |
| 228 | 226 | #end |
| 229 | 227 | </el-checkbox-group> |
| 230 | 228 | </el-form-item> |
| ... | ... | @@ -240,7 +238,7 @@ |
| 240 | 238 | {{ dict.label }} |
| 241 | 239 | </el-radio> |
| 242 | 240 | #else##没数据字典 |
| 243 | - <el-radio label="1">请选择字典生成</el-radio> | |
| 241 | + <el-radio value="1">请选择字典生成</el-radio> | |
| 244 | 242 | #end |
| 245 | 243 | </el-radio-group> |
| 246 | 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 | 75 | <el-checkbox |
| 76 | 76 | v-for="dict in $dictMethod(DICT_TYPE.$dictType.toUpperCase())" |
| 77 | 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 | 81 | #else##没数据字典 |
| 83 | - <el-checkbox>请选择字典生成</el-checkbox> | |
| 82 | + <el-checkbox label="请选择字典生成" /> | |
| 84 | 83 | #end |
| 85 | 84 | </el-checkbox-group> |
| 86 | 85 | </el-form-item> |
| ... | ... | @@ -96,7 +95,7 @@ |
| 96 | 95 | {{ dict.label }} |
| 97 | 96 | </el-radio> |
| 98 | 97 | #else##没数据字典 |
| 99 | - <el-radio label="1">请选择字典生成</el-radio> | |
| 98 | + <el-radio value="1">请选择字典生成</el-radio> | |
| 100 | 99 | #end |
| 101 | 100 | </el-radio-group> |
| 102 | 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 | 3 | import cn.iocoder.yudao.module.product.enums.spu.ProductSpuStatusEnum; |
| 4 | 4 | import lombok.Data; |
| 5 | 5 | |
| 6 | +import java.util.List; | |
| 7 | + | |
| 6 | 8 | /** |
| 7 | 9 | * 商品 SPU 信息 Response DTO |
| 8 | 10 | * |
| ... | ... | @@ -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 | 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 | 67 | // 校验 SPU |
| 68 | 68 | ProductSpuDO spu = validateSpu(sku.getSpuId()); |
| 69 | 69 | // 校验评论 |
| 70 | - validateCommentExists(createReqDTO.getUserId(), createReqDTO.getOrderId()); | |
| 70 | + validateCommentExists(createReqDTO.getUserId(), createReqDTO.getOrderItemId()); | |
| 71 | 71 | // 获取用户详细信息 |
| 72 | 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 | 3 | import cn.iocoder.yudao.module.promotion.api.coupon.dto.CouponRespDTO; |
| 4 | 4 | import cn.iocoder.yudao.module.promotion.api.coupon.dto.CouponUseReqDTO; |
| 5 | 5 | import cn.iocoder.yudao.module.promotion.api.coupon.dto.CouponValidReqDTO; |
| 6 | - | |
| 7 | 6 | import jakarta.validation.Valid; |
| 8 | 7 | |
| 8 | +import java.util.List; | |
| 9 | +import java.util.Map; | |
| 10 | + | |
| 9 | 11 | /** |
| 10 | 12 | * 优惠劵 API 接口 |
| 11 | 13 | * |
| ... | ... | @@ -35,4 +37,21 @@ public interface CouponApi { |
| 35 | 37 | */ |
| 36 | 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 | 1 | package cn.iocoder.yudao.module.promotion.api.reward.dto; |
| 2 | 2 | |
| 3 | +import cn.iocoder.yudao.framework.common.enums.CommonStatusEnum; | |
| 3 | 4 | import cn.iocoder.yudao.module.promotion.enums.common.PromotionConditionTypeEnum; |
| 5 | +import cn.iocoder.yudao.module.promotion.enums.common.PromotionProductScopeEnum; | |
| 4 | 6 | import lombok.Data; |
| 5 | 7 | |
| 8 | +import java.io.Serializable; | |
| 9 | +import java.time.LocalDateTime; | |
| 6 | 10 | import java.util.List; |
| 11 | +import java.util.Map; | |
| 7 | 12 | |
| 8 | 13 | /** |
| 9 | 14 | * 满减送活动的匹配 Response DTO |
| ... | ... | @@ -22,27 +27,49 @@ public class RewardActivityMatchRespDTO { |
| 22 | 27 | */ |
| 23 | 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 | 50 | * 枚举 {@link PromotionConditionTypeEnum} |
| 28 | 51 | */ |
| 29 | 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 | 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 | 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 | 91 | */ |
| 65 | 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 | 44 | ErrorCode REWARD_ACTIVITY_UPDATE_FAIL_STATUS_CLOSED = new ErrorCode(1_013_006_002, "满减送活动已关闭,不能修改"); |
| 45 | 45 | ErrorCode REWARD_ACTIVITY_DELETE_FAIL_STATUS_NOT_CLOSED = new ErrorCode(1_013_006_003, "满减送活动未关闭,不能删除"); |
| 46 | 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 | 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 | 5 | import lombok.Getter; |
| 6 | 6 | |
| 7 | 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 | 16 | @AllArgsConstructor |
| 16 | 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 | 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 | 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 | 17 | |
| 18 | 18 | UNUSED(1, "未使用"), |
| 19 | 19 | USED(2, "已使用"), |
| 20 | - EXPIRE(3, "已过期"), | |
| 21 | - ; | |
| 20 | + EXPIRE(3, "已过期"); | |
| 22 | 21 | |
| 23 | 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 | 7 | import cn.iocoder.yudao.module.promotion.convert.coupon.CouponConvert; |
| 8 | 8 | import cn.iocoder.yudao.module.promotion.dal.dataobject.coupon.CouponDO; |
| 9 | 9 | import cn.iocoder.yudao.module.promotion.service.coupon.CouponService; |
| 10 | +import jakarta.annotation.Resource; | |
| 10 | 11 | import org.springframework.stereotype.Service; |
| 11 | 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 | 18 | * 优惠劵 API 实现类 |
| ... | ... | @@ -41,4 +43,14 @@ public class CouponApiImpl implements CouponApi { |
| 41 | 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 | 2 | |
| 3 | 3 | import cn.iocoder.yudao.framework.common.pojo.CommonResult; |
| 4 | 4 | import cn.iocoder.yudao.framework.common.pojo.PageResult; |
| 5 | +import cn.iocoder.yudao.framework.common.util.object.BeanUtils; | |
| 5 | 6 | import cn.iocoder.yudao.module.promotion.controller.admin.reward.vo.RewardActivityCreateReqVO; |
| 6 | 7 | import cn.iocoder.yudao.module.promotion.controller.admin.reward.vo.RewardActivityPageReqVO; |
| 7 | 8 | import cn.iocoder.yudao.module.promotion.controller.admin.reward.vo.RewardActivityRespVO; |
| 8 | 9 | import cn.iocoder.yudao.module.promotion.controller.admin.reward.vo.RewardActivityUpdateReqVO; |
| 9 | -import cn.iocoder.yudao.module.promotion.convert.reward.RewardActivityConvert; | |
| 10 | 10 | import cn.iocoder.yudao.module.promotion.dal.dataobject.reward.RewardActivityDO; |
| 11 | 11 | import cn.iocoder.yudao.module.promotion.service.reward.RewardActivityService; |
| 12 | 12 | import io.swagger.v3.oas.annotations.Operation; |
| 13 | 13 | import io.swagger.v3.oas.annotations.Parameter; |
| 14 | 14 | import io.swagger.v3.oas.annotations.tags.Tag; |
| 15 | +import jakarta.annotation.Resource; | |
| 16 | +import jakarta.validation.Valid; | |
| 15 | 17 | import org.springframework.security.access.prepost.PreAuthorize; |
| 16 | 18 | import org.springframework.validation.annotation.Validated; |
| 17 | 19 | import org.springframework.web.bind.annotation.*; |
| 18 | 20 | |
| 19 | -import jakarta.annotation.Resource; | |
| 20 | -import jakarta.validation.Valid; | |
| 21 | - | |
| 22 | 21 | import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success; |
| 23 | 22 | |
| 24 | 23 | @Tag(name = "管理后台 - 满减送活动") |
| ... | ... | @@ -69,7 +68,7 @@ public class RewardActivityController { |
| 69 | 68 | @PreAuthorize("@ss.hasPermission('promotion:reward-activity:query')") |
| 70 | 69 | public CommonResult<RewardActivityRespVO> getRewardActivity(@RequestParam("id") Long id) { |
| 71 | 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 | 74 | @GetMapping("/page") |
| ... | ... | @@ -77,7 +76,7 @@ public class RewardActivityController { |
| 77 | 76 | @PreAuthorize("@ss.hasPermission('promotion:reward-activity:query')") |
| 78 | 77 | public CommonResult<PageResult<RewardActivityRespVO>> getRewardActivityPage(@Valid RewardActivityPageReqVO pageVO) { |
| 79 | 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 | 6 | import cn.iocoder.yudao.module.promotion.enums.common.PromotionProductScopeEnum; |
| 7 | 7 | import com.fasterxml.jackson.annotation.JsonIgnore; |
| 8 | 8 | import io.swagger.v3.oas.annotations.media.Schema; |
| 9 | -import lombok.Data; | |
| 10 | -import org.springframework.format.annotation.DateTimeFormat; | |
| 11 | - | |
| 12 | 9 | import jakarta.validation.Valid; |
| 13 | 10 | import jakarta.validation.constraints.AssertTrue; |
| 14 | 11 | import jakarta.validation.constraints.Future; |
| 15 | 12 | import jakarta.validation.constraints.Min; |
| 16 | 13 | import jakarta.validation.constraints.NotNull; |
| 14 | +import lombok.Data; | |
| 15 | + | |
| 17 | 16 | import java.time.LocalDateTime; |
| 18 | 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 | 22 | * 满减送活动 Base VO,提供给添加、修改、详细的子 VO 使用 |
| ... | ... | @@ -32,12 +31,10 @@ public class RewardActivityBaseVO { |
| 32 | 31 | |
| 33 | 32 | @Schema(description = "开始时间", requiredMode = Schema.RequiredMode.REQUIRED) |
| 34 | 33 | @NotNull(message = "开始时间不能为空") |
| 35 | - @DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND) | |
| 36 | 34 | private LocalDateTime startTime; |
| 37 | 35 | |
| 38 | 36 | @Schema(description = "结束时间", requiredMode = Schema.RequiredMode.REQUIRED) |
| 39 | 37 | @NotNull(message = "结束时间不能为空") |
| 40 | - @DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND) | |
| 41 | 38 | @Future(message = "结束时间必须大于当前时间") |
| 42 | 39 | private LocalDateTime endTime; |
| 43 | 40 | |
| ... | ... | @@ -54,8 +51,8 @@ public class RewardActivityBaseVO { |
| 54 | 51 | @InEnum(value = PromotionProductScopeEnum.class, message = "商品范围必须是 {value}") |
| 55 | 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 | 73 | private Integer discountPrice; |
| 77 | 74 | |
| 78 | 75 | @Schema(description = "是否包邮", requiredMode = Schema.RequiredMode.REQUIRED, example = "true") |
| 76 | + @NotNull(message = "规则是否包邮不能为空") | |
| 79 | 77 | private Boolean freeDelivery; |
| 80 | 78 | |
| 81 | 79 | @Schema(description = "赠送的积分", requiredMode = Schema.RequiredMode.REQUIRED, example = "100") |
| 82 | - @Min(value = 1L, message = "赠送的积分必须大于等于 1") | |
| 83 | 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 | 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 | 2 | |
| 3 | 3 | import cn.hutool.core.collection.CollUtil; |
| 4 | 4 | import cn.hutool.core.map.MapUtil; |
| 5 | +import cn.hutool.core.util.ObjUtil; | |
| 5 | 6 | import cn.iocoder.yudao.framework.common.enums.CommonStatusEnum; |
| 6 | 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 | 10 | import cn.iocoder.yudao.module.promotion.controller.app.activity.vo.AppActivityRespVO; |
| 8 | 11 | import cn.iocoder.yudao.module.promotion.dal.dataobject.bargain.BargainActivityDO; |
| 9 | 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 | 14 | import cn.iocoder.yudao.module.promotion.dal.dataobject.discount.DiscountProductDO; |
| 12 | 15 | import cn.iocoder.yudao.module.promotion.dal.dataobject.reward.RewardActivityDO; |
| 13 | 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 | 18 | import cn.iocoder.yudao.module.promotion.enums.common.PromotionTypeEnum; |
| 16 | 19 | import cn.iocoder.yudao.module.promotion.service.bargain.BargainActivityService; |
| 17 | 20 | import cn.iocoder.yudao.module.promotion.service.combination.CombinationActivityService; |
| ... | ... | @@ -30,7 +33,6 @@ import org.springframework.web.bind.annotation.RestController; |
| 30 | 33 | |
| 31 | 34 | import java.time.LocalDateTime; |
| 32 | 35 | import java.util.*; |
| 33 | -import java.util.stream.Collectors; | |
| 34 | 36 | |
| 35 | 37 | import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success; |
| 36 | 38 | import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.*; |
| ... | ... | @@ -52,6 +54,9 @@ public class AppActivityController { |
| 52 | 54 | @Resource |
| 53 | 55 | private RewardActivityService rewardActivityService; |
| 54 | 56 | |
| 57 | + @Resource | |
| 58 | + private ProductSpuApi productSpuApi; | |
| 59 | + | |
| 55 | 60 | @GetMapping("/list-by-spu-id") |
| 56 | 61 | @Operation(summary = "获得单个商品,近期参与的每个活动") |
| 57 | 62 | @Parameter(name = "spuId", description = "商品编号", required = true) |
| ... | ... | @@ -87,7 +92,7 @@ public class AppActivityController { |
| 87 | 92 | // 4. 限时折扣活动 |
| 88 | 93 | getDiscountActivities(spuIds, now, activityList); |
| 89 | 94 | // 5. 满减送活动 |
| 90 | - getRewardActivities(spuIds, now, activityList); | |
| 95 | + getRewardActivityList(spuIds, now, activityList); | |
| 91 | 96 | return activityList; |
| 92 | 97 | } |
| 93 | 98 | |
| ... | ... | @@ -144,28 +149,51 @@ public class AppActivityController { |
| 144 | 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 | 156 | if (CollUtil.isEmpty(rewardActivityList)) { |
| 152 | 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
yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/dal/dataobject/reward/RewardActivityDO.java
| 1 | 1 | package cn.iocoder.yudao.module.promotion.dal.dataobject.reward; |
| 2 | 2 | |
| 3 | +import cn.iocoder.yudao.framework.common.enums.CommonStatusEnum; | |
| 3 | 4 | import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO; |
| 4 | 5 | import cn.iocoder.yudao.framework.mybatis.core.type.LongListTypeHandler; |
| 5 | -import cn.iocoder.yudao.module.promotion.enums.common.PromotionActivityStatusEnum; | |
| 6 | 6 | import cn.iocoder.yudao.module.promotion.enums.common.PromotionConditionTypeEnum; |
| 7 | 7 | import cn.iocoder.yudao.module.promotion.enums.common.PromotionProductScopeEnum; |
| 8 | 8 | import com.baomidou.mybatisplus.annotation.KeySequence; |
| ... | ... | @@ -16,6 +16,7 @@ import lombok.EqualsAndHashCode; |
| 16 | 16 | import java.io.Serializable; |
| 17 | 17 | import java.time.LocalDateTime; |
| 18 | 18 | import java.util.List; |
| 19 | +import java.util.Map; | |
| 19 | 20 | |
| 20 | 21 | /** |
| 21 | 22 | * 满减送活动 DO |
| ... | ... | @@ -40,7 +41,7 @@ public class RewardActivityDO extends BaseDO { |
| 40 | 41 | /** |
| 41 | 42 | * 状态 |
| 42 | 43 | * |
| 43 | - * 枚举 {@link PromotionActivityStatusEnum} | |
| 44 | + * 枚举 {@link CommonStatusEnum} | |
| 44 | 45 | */ |
| 45 | 46 | private Integer status; |
| 46 | 47 | /** |
| ... | ... | @@ -71,7 +72,7 @@ public class RewardActivityDO extends BaseDO { |
| 71 | 72 | * 商品 SPU 编号的数组 |
| 72 | 73 | */ |
| 73 | 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 | 105 | */ |
| 105 | 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 | 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 | 33 | default List<RewardActivityDO> selectListByProductScopeAndStatus(Integer productScope, Integer status) { |
| 38 | 34 | return selectList(new LambdaQueryWrapperX<RewardActivityDO>() |
| 39 | 35 | .eq(RewardActivityDO::getProductScope, productScope) |
| ... | ... | @@ -53,16 +49,16 @@ public interface RewardActivityMapper extends BaseMapperX<RewardActivityDO> { |
| 53 | 49 | * 获取指定活动编号的活动列表且 |
| 54 | 50 | * 开始时间和结束时间小于给定时间 dateTime 的活动列表 |
| 55 | 51 | * |
| 56 | - * @param ids 活动编号 | |
| 52 | + * @param status 状态 | |
| 57 | 53 | * @param dateTime 指定日期 |
| 58 | 54 | * @return 活动列表 |
| 59 | 55 | */ |
| 60 | - default List<RewardActivityDO> selectListByIdsAndDateTimeLt(Collection<Long> ids, LocalDateTime dateTime) { | |
| 56 | + default List<RewardActivityDO> selectListByStatusAndDateTimeLt(Integer status, LocalDateTime dateTime) { | |
| 61 | 57 | return selectList(new LambdaQueryWrapperX<RewardActivityDO>() |
| 62 | - .in(RewardActivityDO::getId, ids) | |
| 58 | + .eq(RewardActivityDO::getStatus, status) | |
| 63 | 59 | .lt(RewardActivityDO::getStartTime, dateTime) |
| 64 | 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 | 27 | import cn.iocoder.yudao.module.system.api.social.SocialClientApi; |
| 28 | 28 | import cn.iocoder.yudao.module.system.api.social.dto.SocialWxaSubscribeMessageSendReqDTO; |
| 29 | 29 | import cn.iocoder.yudao.module.trade.api.order.TradeOrderApi; |
| 30 | +import cn.iocoder.yudao.module.trade.enums.order.TradeOrderCancelTypeEnum; | |
| 30 | 31 | import jakarta.annotation.Nullable; |
| 31 | 32 | import jakarta.annotation.Resource; |
| 32 | 33 | import lombok.extern.slf4j.Slf4j; |
| ... | ... | @@ -37,7 +38,10 @@ import org.springframework.transaction.annotation.Transactional; |
| 37 | 38 | import org.springframework.validation.annotation.Validated; |
| 38 | 39 | |
| 39 | 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 | 46 | import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception; |
| 43 | 47 | import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.*; |
| ... | ... | @@ -335,7 +339,8 @@ public class CombinationRecordServiceImpl implements CombinationRecordService { |
| 335 | 339 | List<CombinationRecordDO> headAndRecords = updateBatchCombinationRecords(headRecord, |
| 336 | 340 | CombinationRecordStatusEnum.FAILED); |
| 337 | 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 | 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 | 44 | * @param id 优惠劵编号 |
| ... | ... | @@ -70,42 +62,44 @@ public interface CouponService { |
| 70 | 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 | 67 | * @param templateId 优惠券模板编号 |
| 93 | 68 | * @param userIds 用户编号列表 |
| 94 | 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 | 77 | * @param templateId 优惠券模板编号 |
| 102 | 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 | 105 | * @param templateId 优惠券模板编号 |
| ... | ... | @@ -123,16 +117,38 @@ public interface CouponService { |
| 123 | 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 | 160 | Map<Long, Integer> getTakeCountMapByTemplateIds(Collection<Long> templateIds, Long userId); |
| 145 | 161 | |
| 146 | 162 | /** |
| 147 | - * 获取用户匹配的优惠券列表 | |
| 163 | + * 获取会员领取指定优惠券的数量 | |
| 148 | 164 | * |
| 165 | + * @param templateId 优惠券模板编号 | |
| 149 | 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 | 3 | import cn.hutool.core.collection.CollStreamUtil; |
| 4 | 4 | import cn.hutool.core.collection.CollUtil; |
| 5 | 5 | import cn.hutool.core.map.MapUtil; |
| 6 | +import cn.hutool.core.util.ObjUtil; | |
| 6 | 7 | import cn.hutool.core.util.ObjectUtil; |
| 7 | 8 | import cn.hutool.core.util.StrUtil; |
| 8 | 9 | import cn.hutool.extra.spring.SpringUtil; |
| ... | ... | @@ -19,19 +20,19 @@ import cn.iocoder.yudao.module.promotion.dal.mysql.coupon.CouponMapper; |
| 19 | 20 | import cn.iocoder.yudao.module.promotion.enums.coupon.CouponStatusEnum; |
| 20 | 21 | import cn.iocoder.yudao.module.promotion.enums.coupon.CouponTakeTypeEnum; |
| 21 | 22 | import cn.iocoder.yudao.module.promotion.enums.coupon.CouponTemplateValidityTypeEnum; |
| 23 | +import jakarta.annotation.Resource; | |
| 22 | 24 | import lombok.extern.slf4j.Slf4j; |
| 23 | 25 | import org.springframework.stereotype.Service; |
| 24 | 26 | import org.springframework.transaction.annotation.Transactional; |
| 25 | 27 | import org.springframework.validation.annotation.Validated; |
| 26 | 28 | |
| 27 | -import jakarta.annotation.Resource; | |
| 28 | - | |
| 29 | 29 | import java.time.LocalDateTime; |
| 30 | 30 | import java.util.*; |
| 31 | 31 | import java.util.stream.Collectors; |
| 32 | 32 | |
| 33 | 33 | import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception; |
| 34 | 34 | import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.*; |
| 35 | +import static cn.iocoder.yudao.framework.common.util.collection.MapUtils.findAndThen; | |
| 35 | 36 | import static cn.iocoder.yudao.module.promotion.enums.ErrorCodeConstants.*; |
| 36 | 37 | import static java.util.Arrays.asList; |
| 37 | 38 | |
| ... | ... | @@ -77,20 +78,6 @@ public class CouponServiceImpl implements CouponService { |
| 77 | 78 | } |
| 78 | 79 | |
| 79 | 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 | 81 | public void useCoupon(Long id, Long userId, Long orderId) { |
| 95 | 82 | // 校验优惠劵 |
| 96 | 83 | validCoupon(id, userId); |
| ... | ... | @@ -147,25 +134,8 @@ public class CouponServiceImpl implements CouponService { |
| 147 | 134 | } |
| 148 | 135 | |
| 149 | 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 | 139 | CouponTemplateDO template = couponTemplateService.getCouponTemplate(templateId); |
| 170 | 140 | // 1. 过滤掉达到领取限制的用户 |
| 171 | 141 | removeTakeLimitUser(userIds, template); |
| ... | ... | @@ -173,37 +143,86 @@ public class CouponServiceImpl implements CouponService { |
| 173 | 143 | validateCouponTemplateCanTake(template, userIds, takeType); |
| 174 | 144 | |
| 175 | 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 | 150 | couponTemplateService.updateCouponTemplateTakeCount(templateId, userIds.size()); |
| 151 | + | |
| 152 | + return convertMultiMap(couponList, CouponDO::getUserId, CouponDO::getId); | |
| 180 | 153 | } |
| 181 | 154 | |
| 182 | 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 | 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 | 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 | 228 | @Override |
| ... | ... | @@ -230,27 +249,6 @@ public class CouponServiceImpl implements CouponService { |
| 230 | 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 | 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 | 388 | @Override |
| 326 | 389 | public CouponDO getCoupon(Long userId, Long id) { |
| 327 | 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 | 402 | * 获得自身的代理对象,解决 AOP 生效问题 |
| 332 | 403 | * |
| ... | ... | @@ -335,4 +406,5 @@ public class CouponServiceImpl implements CouponService { |
| 335 | 406 | private CouponServiceImpl getSelf() { |
| 336 | 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 | 104 | } |
| 105 | 105 | // 计算新增的记录 |
| 106 | 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 | 111 | newDiscountProducts.removeIf(product -> dbDiscountProducts.stream().anyMatch( |
| 109 | 112 | dbProduct -> DiscountActivityConvert.INSTANCE.isEquals(dbProduct, product))); // 如果匹配到,说明是更新的 |
| 110 | 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 | 75 | /** |
| 76 | 76 | * 获取指定 spu 编号最近参加的活动,每个 spuId 只返回一条记录 |
| 77 | 77 | * |
| 78 | - * @param spuIds spu 编号 | |
| 79 | 78 | * @param status 状态 |
| 80 | 79 | * @param dateTime 当前日期时间 |
| 81 | 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 | 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 | 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 | 8 | import cn.iocoder.yudao.module.promotion.api.reward.dto.RewardActivityMatchRespDTO; |
| 9 | +import cn.iocoder.yudao.module.promotion.controller.admin.reward.vo.RewardActivityBaseVO; | |
| 6 | 10 | import cn.iocoder.yudao.module.promotion.controller.admin.reward.vo.RewardActivityCreateReqVO; |
| 7 | 11 | import cn.iocoder.yudao.module.promotion.controller.admin.reward.vo.RewardActivityPageReqVO; |
| 8 | 12 | import cn.iocoder.yudao.module.promotion.controller.admin.reward.vo.RewardActivityUpdateReqVO; |
| 9 | -import cn.iocoder.yudao.module.promotion.convert.reward.RewardActivityConvert; | |
| 10 | 13 | import cn.iocoder.yudao.module.promotion.dal.dataobject.reward.RewardActivityDO; |
| 11 | 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 | 16 | import cn.iocoder.yudao.module.promotion.util.PromotionUtils; |
| 14 | 17 | import jakarta.annotation.Resource; |
| 15 | 18 | import org.springframework.stereotype.Service; |
| ... | ... | @@ -17,13 +20,13 @@ import org.springframework.validation.annotation.Validated; |
| 17 | 20 | |
| 18 | 21 | import java.time.LocalDateTime; |
| 19 | 22 | import java.util.Collection; |
| 20 | -import java.util.Collections; | |
| 21 | 23 | import java.util.List; |
| 24 | +import java.util.Objects; | |
| 22 | 25 | |
| 26 | +import static cn.hutool.core.collection.CollUtil.intersectionDistinct; | |
| 23 | 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 | 29 | import static cn.iocoder.yudao.module.promotion.enums.ErrorCodeConstants.*; |
| 26 | -import static java.util.Arrays.asList; | |
| 27 | 30 | |
| 28 | 31 | /** |
| 29 | 32 | * 满减送活动 Service 实现类 |
| ... | ... | @@ -37,13 +40,20 @@ public class RewardActivityServiceImpl implements RewardActivityService { |
| 37 | 40 | @Resource |
| 38 | 41 | private RewardActivityMapper rewardActivityMapper; |
| 39 | 42 | |
| 43 | + @Resource | |
| 44 | + private ProductCategoryApi productCategoryApi; | |
| 45 | + @Resource | |
| 46 | + private ProductSpuApi productSpuApi; | |
| 47 | + | |
| 40 | 48 | @Override |
| 41 | 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 | 57 | .setStatus(PromotionUtils.calculateActivityStatus(createReqVO.getEndTime())); |
| 48 | 58 | rewardActivityMapper.insert(rewardActivity); |
| 49 | 59 | // 返回 |
| ... | ... | @@ -52,16 +62,18 @@ public class RewardActivityServiceImpl implements RewardActivityService { |
| 52 | 62 | |
| 53 | 63 | @Override |
| 54 | 64 | public void updateRewardActivity(RewardActivityUpdateReqVO updateReqVO) { |
| 55 | - // 校验存在 | |
| 65 | + // 1.1 校验存在 | |
| 56 | 66 | RewardActivityDO dbRewardActivity = validateRewardActivityExists(updateReqVO.getId()); |
| 57 | - if (dbRewardActivity.getStatus().equals(PromotionActivityStatusEnum.CLOSE.getStatus())) { // 已关闭的活动,不能修改噢 | |
| 67 | + if (dbRewardActivity.getStatus().equals(CommonStatusEnum.DISABLE.getStatus())) { // 已关闭的活动,不能修改噢 | |
| 58 | 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 | 77 | .setStatus(PromotionUtils.calculateActivityStatus(updateReqVO.getEndTime())); |
| 66 | 78 | rewardActivityMapper.updateById(updateObj); |
| 67 | 79 | } |
| ... | ... | @@ -70,15 +82,12 @@ public class RewardActivityServiceImpl implements RewardActivityService { |
| 70 | 82 | public void closeRewardActivity(Long id) { |
| 71 | 83 | // 校验存在 |
| 72 | 84 | RewardActivityDO dbRewardActivity = validateRewardActivityExists(id); |
| 73 | - if (dbRewardActivity.getStatus().equals(PromotionActivityStatusEnum.CLOSE.getStatus())) { // 已关闭的活动,不能关闭噢 | |
| 85 | + if (dbRewardActivity.getStatus().equals(CommonStatusEnum.DISABLE.getStatus())) { // 已关闭的活动,不能关闭噢 | |
| 74 | 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 | 91 | rewardActivityMapper.updateById(updateObj); |
| 83 | 92 | } |
| 84 | 93 | |
| ... | ... | @@ -86,7 +95,7 @@ public class RewardActivityServiceImpl implements RewardActivityService { |
| 86 | 95 | public void deleteRewardActivity(Long id) { |
| 87 | 96 | // 校验存在 |
| 88 | 97 | RewardActivityDO dbRewardActivity = validateRewardActivityExists(id); |
| 89 | - if (!dbRewardActivity.getStatus().equals(PromotionActivityStatusEnum.CLOSE.getStatus())) { // 未关闭的活动,不能删除噢 | |
| 98 | + if (dbRewardActivity.getStatus().equals(CommonStatusEnum.ENABLE.getStatus())) { // 未关闭的活动,不能删除噢 | |
| 90 | 99 | throw exception(REWARD_ACTIVITY_DELETE_FAIL_STATUS_NOT_CLOSED); |
| 91 | 100 | } |
| 92 | 101 | |
| ... | ... | @@ -102,41 +111,39 @@ public class RewardActivityServiceImpl implements RewardActivityService { |
| 102 | 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 | 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 | 149 | @Override |
| ... | ... | @@ -151,32 +158,13 @@ public class RewardActivityServiceImpl implements RewardActivityService { |
| 151 | 158 | |
| 152 | 159 | @Override |
| 153 | 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 | 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 | 1 | package cn.iocoder.yudao.module.promotion.service.reward; |
| 2 | 2 | |
| 3 | +import cn.iocoder.yudao.framework.common.enums.CommonStatusEnum; | |
| 3 | 4 | import cn.iocoder.yudao.framework.common.pojo.PageResult; |
| 4 | 5 | import cn.iocoder.yudao.framework.test.core.ut.BaseDbUnitTest; |
| 6 | +import cn.iocoder.yudao.module.promotion.api.reward.dto.RewardActivityMatchRespDTO; | |
| 5 | 7 | import cn.iocoder.yudao.module.promotion.controller.admin.reward.vo.RewardActivityCreateReqVO; |
| 6 | 8 | import cn.iocoder.yudao.module.promotion.controller.admin.reward.vo.RewardActivityPageReqVO; |
| 7 | 9 | import cn.iocoder.yudao.module.promotion.controller.admin.reward.vo.RewardActivityUpdateReqVO; |
| 8 | 10 | import cn.iocoder.yudao.module.promotion.dal.dataobject.reward.RewardActivityDO; |
| 9 | 11 | import cn.iocoder.yudao.module.promotion.dal.mysql.reward.RewardActivityMapper; |
| 10 | -import cn.iocoder.yudao.module.promotion.enums.common.PromotionActivityStatusEnum; | |
| 11 | 12 | import cn.iocoder.yudao.module.promotion.enums.common.PromotionConditionTypeEnum; |
| 12 | 13 | import cn.iocoder.yudao.module.promotion.enums.common.PromotionProductScopeEnum; |
| 14 | +import jakarta.annotation.Resource; | |
| 13 | 15 | import org.junit.jupiter.api.Disabled; |
| 14 | 16 | import org.junit.jupiter.api.Test; |
| 15 | 17 | import org.springframework.context.annotation.Import; |
| 16 | 18 | |
| 17 | -import jakarta.annotation.Resource; | |
| 18 | 19 | import java.time.Duration; |
| 20 | +import java.util.List; | |
| 19 | 21 | import java.util.Set; |
| 20 | 22 | |
| 21 | 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 | 29 | import static cn.iocoder.yudao.framework.test.core.util.RandomUtils.randomLongId; |
| 28 | 30 | import static cn.iocoder.yudao.framework.test.core.util.RandomUtils.randomPojo; |
| 29 | 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 | 33 | import static java.util.Collections.singletonList; |
| 32 | 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 | 41 | @Disabled // TODO 芋艿:后续 fix 补充的单测 |
| 40 | 42 | @Import(RewardActivityServiceImpl.class) |
| 41 | 43 | public class RewardActivityServiceImplTest extends BaseDbUnitTest { |
| ... | ... | @@ -63,7 +65,7 @@ public class RewardActivityServiceImplTest extends BaseDbUnitTest { |
| 63 | 65 | // 校验记录的属性是否正确 |
| 64 | 66 | RewardActivityDO rewardActivity = rewardActivityMapper.selectById(rewardActivityId); |
| 65 | 67 | assertPojoEquals(reqVO, rewardActivity, "rules"); |
| 66 | - assertEquals(rewardActivity.getStatus(), PromotionActivityStatusEnum.WAIT.getStatus()); | |
| 68 | + assertEquals(rewardActivity.getStatus(), CommonStatusEnum.DISABLE.getStatus()); | |
| 67 | 69 | for (int i = 0; i < reqVO.getRules().size(); i++) { |
| 68 | 70 | assertPojoEquals(reqVO.getRules().get(i), rewardActivity.getRules().get(i)); |
| 69 | 71 | } |
| ... | ... | @@ -72,7 +74,7 @@ public class RewardActivityServiceImplTest extends BaseDbUnitTest { |
| 72 | 74 | @Test |
| 73 | 75 | public void testUpdateRewardActivity_success() { |
| 74 | 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 | 78 | rewardActivityMapper.insert(dbRewardActivity);// @Sql: 先插入出一条存在的数据 |
| 77 | 79 | // 准备参数 |
| 78 | 80 | RewardActivityUpdateReqVO reqVO = randomPojo(RewardActivityUpdateReqVO.class, o -> { |
| ... | ... | @@ -88,7 +90,7 @@ public class RewardActivityServiceImplTest extends BaseDbUnitTest { |
| 88 | 90 | // 校验是否更新正确 |
| 89 | 91 | RewardActivityDO rewardActivity = rewardActivityMapper.selectById(reqVO.getId()); // 获取最新的 |
| 90 | 92 | assertPojoEquals(reqVO, rewardActivity, "rules"); |
| 91 | - assertEquals(rewardActivity.getStatus(), PromotionActivityStatusEnum.WAIT.getStatus()); | |
| 93 | + assertEquals(rewardActivity.getStatus(), CommonStatusEnum.DISABLE.getStatus()); | |
| 92 | 94 | for (int i = 0; i < reqVO.getRules().size(); i++) { |
| 93 | 95 | assertPojoEquals(reqVO.getRules().get(i), rewardActivity.getRules().get(i)); |
| 94 | 96 | } |
| ... | ... | @@ -97,7 +99,7 @@ public class RewardActivityServiceImplTest extends BaseDbUnitTest { |
| 97 | 99 | @Test |
| 98 | 100 | public void testCloseRewardActivity() { |
| 99 | 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 | 103 | rewardActivityMapper.insert(dbRewardActivity);// @Sql: 先插入出一条存在的数据 |
| 102 | 104 | // 准备参数 |
| 103 | 105 | Long id = dbRewardActivity.getId(); |
| ... | ... | @@ -106,7 +108,7 @@ public class RewardActivityServiceImplTest extends BaseDbUnitTest { |
| 106 | 108 | rewardActivityService.closeRewardActivity(id); |
| 107 | 109 | // 校验状态 |
| 108 | 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 | 114 | @Test |
| ... | ... | @@ -121,15 +123,15 @@ public class RewardActivityServiceImplTest extends BaseDbUnitTest { |
| 121 | 123 | @Test |
| 122 | 124 | public void testDeleteRewardActivity_success() { |
| 123 | 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 | 127 | rewardActivityMapper.insert(dbRewardActivity);// @Sql: 先插入出一条存在的数据 |
| 126 | 128 | // 准备参数 |
| 127 | 129 | Long id = dbRewardActivity.getId(); |
| 128 | 130 | |
| 129 | 131 | // 调用 |
| 130 | 132 | rewardActivityService.deleteRewardActivity(id); |
| 131 | - // 校验数据不存在了 | |
| 132 | - assertNull(rewardActivityMapper.selectById(id)); | |
| 133 | + // 校验数据不存在了 | |
| 134 | + assertNull(rewardActivityMapper.selectById(id)); | |
| 133 | 135 | } |
| 134 | 136 | |
| 135 | 137 | @Test |
| ... | ... | @@ -143,77 +145,82 @@ public class RewardActivityServiceImplTest extends BaseDbUnitTest { |
| 143 | 145 | |
| 144 | 146 | @Test |
| 145 | 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 | 171 | @Test |
| 170 | 172 | public void testGetRewardActivities_all() { |
| 171 | 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 | 175 | .setProductScope(PromotionProductScopeEnum.ALL.getScope())); |
| 174 | 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 | 179 | rewardActivityMapper.insert(productActivity); |
| 178 | 180 | // 准备参数 |
| 179 | 181 | Set<Long> spuIds = asSet(1L, 2L); |
| 180 | 182 | |
| 181 | 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 | 197 | @Test |
| 191 | 198 | public void testGetRewardActivities_product() { |
| 192 | 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 | 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 | 205 | rewardActivityMapper.insert(productActivity02); |
| 199 | 206 | // 准备参数 |
| 200 | 207 | Set<Long> spuIds = asSet(1L, 2L, 3L); |
| 201 | 208 | |
| 202 | 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 | 28 | */ |
| 29 | 29 | TradeOrderRespDTO getOrder(Long id); |
| 30 | 30 | |
| 31 | - // TODO 芋艿:需要优化下; | |
| 32 | 31 | /** |
| 33 | 32 | * 取消支付订单 |
| 34 | 33 | * |
| 35 | - * @param userId 用户编号 | |
| 34 | + * @param userId 用户编号 | |
| 36 | 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 | 35 | ErrorCode ORDER_RECEIVE_FAIL_DELIVERY_TYPE_NOT_PICK_UP = new ErrorCode(1_011_000_030, "交易订单自提失败,收货方式不是【用户自提】"); |
| 36 | 36 | ErrorCode ORDER_UPDATE_ADDRESS_FAIL_STATUS_NOT_DELIVERED = new ErrorCode(1_011_000_031, "交易订单修改收货地址失败,原因:订单不是【待发货】状态"); |
| 37 | 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 | 40 | // ========== After Sale 模块 1-011-000-100 ========== |
| 40 | 41 | ErrorCode AFTER_SALE_NOT_FOUND = new ErrorCode(1_011_000_100, "售后单不存在"); |
| ... | ... | @@ -59,6 +60,8 @@ public interface ErrorCodeConstants { |
| 59 | 60 | ErrorCode PRICE_CALCULATE_DELIVERY_PRICE_TEMPLATE_NOT_FOUND = new ErrorCode(1_011_003_002, "计算快递运费异常,找不到对应的运费模板"); |
| 60 | 61 | ErrorCode PRICE_CALCULATE_COUPON_NOT_MATCH_NORMAL_ORDER = new ErrorCode(1_011_003_004, "参与秒杀、拼团、砍价的营销商品,无法使用优惠劵"); |
| 61 | 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 | 66 | // ========== 物流 Express 模块 1-011-004-000 ========== |
| 64 | 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 | 17 | |
| 18 | 18 | PAY_TIMEOUT(10, "超时未支付"), |
| 19 | 19 | AFTER_SALE_CLOSE(20, "退款关闭"), |
| 20 | - MEMBER_CANCEL(30, "买家取消"); | |
| 20 | + MEMBER_CANCEL(30, "买家取消"), | |
| 21 | + COMBINATION_CLOSE(40, "拼团关闭"); | |
| 21 | 22 | |
| 22 | 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 | 4 | import cn.iocoder.yudao.module.trade.convert.order.TradeOrderConvert; |
| 5 | 5 | import cn.iocoder.yudao.module.trade.service.order.TradeOrderQueryService; |
| 6 | 6 | import cn.iocoder.yudao.module.trade.service.order.TradeOrderUpdateService; |
| 7 | +import jakarta.annotation.Resource; | |
| 7 | 8 | import org.springframework.stereotype.Service; |
| 8 | 9 | import org.springframework.validation.annotation.Validated; |
| 9 | 10 | |
| 10 | -import jakarta.annotation.Resource; | |
| 11 | 11 | import java.util.Collection; |
| 12 | 12 | import java.util.List; |
| 13 | 13 | |
| ... | ... | @@ -36,8 +36,8 @@ public class TradeOrderApiImpl implements TradeOrderApi { |
| 36 | 36 | } |
| 37 | 37 | |
| 38 | 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 | 2 | |
| 3 | 3 | import cn.iocoder.yudao.framework.common.enums.TerminalEnum; |
| 4 | 4 | import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO; |
| 5 | +import cn.iocoder.yudao.framework.mybatis.core.type.LongListTypeHandler; | |
| 5 | 6 | import cn.iocoder.yudao.module.member.api.user.dto.MemberUserRespDTO; |
| 6 | 7 | import cn.iocoder.yudao.module.trade.dal.dataobject.brokerage.BrokerageUserDO; |
| 7 | 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 | 13 | import cn.iocoder.yudao.module.trade.enums.order.TradeOrderStatusEnum; |
| 13 | 14 | import cn.iocoder.yudao.module.trade.enums.order.TradeOrderTypeEnum; |
| 14 | 15 | import com.baomidou.mybatisplus.annotation.KeySequence; |
| 16 | +import com.baomidou.mybatisplus.annotation.TableField; | |
| 15 | 17 | import com.baomidou.mybatisplus.annotation.TableName; |
| 18 | +import com.baomidou.mybatisplus.extension.handlers.JacksonTypeHandler; | |
| 16 | 19 | import lombok.*; |
| 17 | 20 | |
| 18 | 21 | import java.time.LocalDateTime; |
| 22 | +import java.util.List; | |
| 23 | +import java.util.Map; | |
| 19 | 24 | |
| 20 | 25 | /** |
| 21 | 26 | * 交易订单 DO |
| ... | ... | @@ -292,6 +297,24 @@ public class TradeOrderDO extends BaseDO { |
| 292 | 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 | 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 | 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 | 272 | if (BrokerageBindModeEnum.REGISTER.getMode().equals(tradeConfig.getBrokerageBindMode())) { |
| 278 | 273 | // 判断是否为新用户:注册时间在 30 秒内的,都算新用户 | ... | ... |
yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/service/order/TradeOrderUpdateService.java
| 1 | 1 | package cn.iocoder.yudao.module.trade.service.order; |
| 2 | 2 | |
| 3 | -import cn.iocoder.yudao.framework.common.enums.TerminalEnum; | |
| 4 | 3 | import cn.iocoder.yudao.module.trade.controller.admin.order.vo.TradeOrderDeliveryReqVO; |
| 5 | 4 | import cn.iocoder.yudao.module.trade.controller.admin.order.vo.TradeOrderRemarkReqVO; |
| 6 | 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 | import cn.iocoder.yudao.module.trade.controller.app.order.vo.AppTradeOrderSettlementRespVO; |
| 11 | 10 | import cn.iocoder.yudao.module.trade.controller.app.order.vo.item.AppTradeOrderItemCommentCreateReqVO; |
| 12 | 11 | import cn.iocoder.yudao.module.trade.dal.dataobject.order.TradeOrderDO; |
| 13 | - | |
| 14 | 12 | import jakarta.validation.constraints.NotNull; |
| 15 | 13 | |
| 14 | +import java.util.List; | |
| 15 | + | |
| 16 | 16 | /** |
| 17 | 17 | * 交易订单【写】Service 接口 |
| 18 | 18 | * |
| ... | ... | @@ -187,13 +187,22 @@ public interface TradeOrderUpdateService { |
| 187 | 187 | */ |
| 188 | 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 | 18 | import cn.iocoder.yudao.module.pay.api.order.PayOrderApi; |
| 19 | 19 | import cn.iocoder.yudao.module.pay.api.order.dto.PayOrderCreateReqDTO; |
| 20 | 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 | 23 | import cn.iocoder.yudao.module.pay.enums.order.PayOrderStatusEnum; |
| 22 | 24 | import cn.iocoder.yudao.module.product.api.comment.ProductCommentApi; |
| 23 | 25 | import cn.iocoder.yudao.module.product.api.comment.dto.ProductCommentCreateReqDTO; |
| ... | ... | @@ -111,6 +113,8 @@ public class TradeOrderUpdateServiceImpl implements TradeOrderUpdateService { |
| 111 | 113 | private ProductCommentApi productCommentApi; |
| 112 | 114 | @Resource |
| 113 | 115 | public SocialClientApi socialClientApi; |
| 116 | + @Resource | |
| 117 | + public PayRefundApi payRefundApi; | |
| 114 | 118 | |
| 115 | 119 | @Resource |
| 116 | 120 | private TradeOrderProperties tradeOrderProperties; |
| ... | ... | @@ -197,6 +201,8 @@ public class TradeOrderUpdateServiceImpl implements TradeOrderUpdateService { |
| 197 | 201 | order.setRefundStatus(TradeOrderRefundStatusEnum.NONE.getStatus()); |
| 198 | 202 | order.setProductCount(getSumValue(calculateRespBO.getItems(), TradePriceCalculateRespBO.OrderItem::getCount, Integer::sum)); |
| 199 | 203 | order.setUserIp(getClientIP()).setTerminal(getTerminal()); |
| 204 | + // 使用 + 赠送优惠券 | |
| 205 | + order.setGiveCouponTemplateCounts(calculateRespBO.getGiveCouponTemplateCounts()); | |
| 200 | 206 | // 支付 + 退款信息 |
| 201 | 207 | order.setAdjustPrice(0).setPayStatus(false); |
| 202 | 208 | order.setRefundStatus(TradeOrderRefundStatusEnum.NONE.getStatus()).setRefundPrice(0); |
| ... | ... | @@ -854,15 +860,46 @@ public class TradeOrderUpdateServiceImpl implements TradeOrderUpdateService { |
| 854 | 860 | |
| 855 | 861 | @Override |
| 856 | 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 | 896 | TradeOrderDO order = tradeOrderMapper.selectOrderByIdAndUserId(orderId, userId); |
| 860 | 897 | if (order == null) { |
| 861 | 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 | 1 | package cn.iocoder.yudao.module.trade.service.order.handler; |
| 2 | 2 | |
| 3 | +import cn.hutool.core.collection.CollUtil; | |
| 3 | 4 | import cn.iocoder.yudao.module.promotion.api.coupon.CouponApi; |
| 4 | 5 | import cn.iocoder.yudao.module.promotion.api.coupon.dto.CouponUseReqDTO; |
| 5 | 6 | import cn.iocoder.yudao.module.trade.dal.dataobject.order.TradeOrderDO; |
| 6 | 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 | 12 | import org.springframework.stereotype.Component; |
| 8 | 13 | |
| 9 | -import jakarta.annotation.Resource; | |
| 10 | 14 | import java.util.List; |
| 11 | 15 | |
| 12 | 16 | /** |
| ... | ... | @@ -18,6 +22,12 @@ import java.util.List; |
| 18 | 22 | public class TradeCouponOrderHandler implements TradeOrderHandler { |
| 19 | 23 | |
| 20 | 24 | @Resource |
| 25 | + @Lazy // 延迟加载,避免循环依赖 | |
| 26 | + private TradeOrderUpdateService orderUpdateService; | |
| 27 | + @Resource | |
| 28 | + private TradeOrderQueryService orderQueryService; | |
| 29 | + | |
| 30 | + @Resource | |
| 21 | 31 | private CouponApi couponApi; |
| 22 | 32 | |
| 23 | 33 | @Override |
| ... | ... | @@ -31,12 +41,30 @@ public class TradeCouponOrderHandler implements TradeOrderHandler { |
| 31 | 41 | } |
| 32 | 42 | |
| 33 | 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 | 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 | 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 | import lombok.Data; |
| 7 | 7 | |
| 8 | 8 | import java.util.List; |
| 9 | +import java.util.Map; | |
| 9 | 10 | |
| 10 | 11 | /** |
| 11 | 12 | * 价格计算 Response BO |
| ... | ... | @@ -68,6 +69,21 @@ public class TradePriceCalculateRespBO { |
| 68 | 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 | 89 | @Data |
| ... | ... | @@ -213,8 +229,19 @@ public class TradePriceCalculateRespBO { |
| 213 | 229 | */ |
| 214 | 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 | 246 | private Long deliveryTemplateId; |
| 220 | 247 | |
| ... | ... | @@ -234,7 +261,7 @@ public class TradePriceCalculateRespBO { |
| 234 | 261 | private List<ProductPropertyValueDetailRespDTO> properties; |
| 235 | 262 | |
| 236 | 263 | /** |
| 237 | - * 使用的积分 | |
| 264 | + * 赠送的积分 | |
| 238 | 265 | */ |
| 239 | 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 | 25 | import static cn.iocoder.yudao.module.promotion.enums.ErrorCodeConstants.COUPON_NO_MATCH_MIN_PRICE; |
| 26 | 26 | import static cn.iocoder.yudao.module.promotion.enums.ErrorCodeConstants.COUPON_NO_MATCH_SPU; |
| 27 | 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 | 31 | * 优惠劵的 {@link TradePriceCalculator} 实现类 |
| ... | ... | @@ -65,8 +66,9 @@ public class TradeCouponPriceCalculator implements TradePriceCalculator { |
| 65 | 66 | |
| 66 | 67 | // 3.1 计算可以优惠的金额 |
| 67 | 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 | 72 | // 3.2 计算分摊的优惠金额 |
| 71 | 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 | 4 | import cn.hutool.core.lang.Assert; |
| 5 | 5 | import cn.hutool.core.util.StrUtil; |
| 6 | 6 | import cn.iocoder.yudao.framework.common.enums.CommonStatusEnum; |
| 7 | +import cn.iocoder.yudao.framework.common.util.collection.CollectionUtils; | |
| 7 | 8 | import cn.iocoder.yudao.module.member.api.address.MemberAddressApi; |
| 8 | 9 | import cn.iocoder.yudao.module.member.api.address.dto.MemberAddressRespDTO; |
| 9 | 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 | 18 | import cn.iocoder.yudao.module.trade.service.price.bo.TradePriceCalculateReqBO; |
| 18 | 19 | import cn.iocoder.yudao.module.trade.service.price.bo.TradePriceCalculateRespBO; |
| 19 | 20 | import cn.iocoder.yudao.module.trade.service.price.bo.TradePriceCalculateRespBO.OrderItem; |
| 21 | +import jakarta.annotation.Resource; | |
| 20 | 22 | import lombok.extern.slf4j.Slf4j; |
| 21 | 23 | import org.springframework.core.annotation.Order; |
| 22 | 24 | import org.springframework.stereotype.Component; |
| 23 | 25 | |
| 24 | -import jakarta.annotation.Resource; | |
| 25 | 26 | import java.util.List; |
| 26 | 27 | import java.util.Map; |
| 27 | 28 | import java.util.Set; |
| ... | ... | @@ -55,7 +56,11 @@ public class TradeDeliveryPriceCalculator implements TradePriceCalculator { |
| 55 | 56 | if (param.getDeliveryType() == null) { |
| 56 | 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 | 64 | if (DeliveryTypeEnum.PICK_UP.getType().equals(param.getDeliveryType())) { |
| 60 | 65 | calculateByPickUp(param); |
| 61 | 66 | } else if (DeliveryTypeEnum.EXPRESS.getType().equals(param.getDeliveryType())) { |
| ... | ... | @@ -90,7 +95,12 @@ public class TradeDeliveryPriceCalculator implements TradePriceCalculator { |
| 90 | 95 | return; |
| 91 | 96 | } |
| 92 | 97 | |
| 93 | - // 情况二:快递模版 | |
| 98 | + // 情况二:活动包邮 | |
| 99 | + if (Boolean.TRUE.equals(result.getFreeDelivery())) { | |
| 100 | + return; | |
| 101 | + } | |
| 102 | + | |
| 103 | + // 情况三:快递模版 | |
| 94 | 104 | // 2.1 过滤出已选中的商品 SKU |
| 95 | 105 | List<OrderItem> selectedItem = filterList(result.getItems(), OrderItem::getSelected); |
| 96 | 106 | Set<Long> deliveryTemplateIds = convertSet(selectedItem, OrderItem::getDeliveryTemplateId); |
| ... | ... | @@ -124,7 +134,7 @@ public class TradeDeliveryPriceCalculator implements TradePriceCalculator { |
| 124 | 134 | Map<Long, List<OrderItem>> template2ItemMap = convertMultiMap(selectedSkus, OrderItem::getDeliveryTemplateId); |
| 125 | 135 | // 依次计算快递运费 |
| 126 | 136 | for (Map.Entry<Long, List<OrderItem>> entry : template2ItemMap.entrySet()) { |
| 127 | - Long templateId = entry.getKey(); | |
| 137 | + Long templateId = entry.getKey(); | |
| 128 | 138 | List<OrderItem> orderItems = entry.getValue(); |
| 129 | 139 | DeliveryExpressTemplateRespBO templateBO = expressTemplateMap.get(templateId); |
| 130 | 140 | if (templateBO == null) { |
| ... | ... | @@ -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 | 159 | * @param templateCharge 快递运费配置 |
| 150 | 160 | */ |
| 151 | 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 | 11 | import cn.iocoder.yudao.module.trade.service.price.bo.TradePriceCalculateRespBO; |
| 12 | 12 | |
| 13 | 13 | import java.util.ArrayList; |
| 14 | +import java.util.LinkedHashMap; | |
| 14 | 15 | import java.util.List; |
| 15 | 16 | import java.util.Map; |
| 16 | 17 | |
| ... | ... | @@ -31,8 +32,7 @@ public class TradePriceCalculatorHelper { |
| 31 | 32 | List<ProductSpuRespDTO> spuList, List<ProductSkuRespDTO> skuList) { |
| 32 | 33 | // 创建 PriceCalculateRespDTO 对象 |
| 33 | 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 | 37 | // 创建它的 OrderItem 属性 |
| 38 | 38 | result.setItems(new ArrayList<>(param.getItems().size())); |
| ... | ... | @@ -60,7 +60,7 @@ public class TradePriceCalculatorHelper { |
| 60 | 60 | .setWeight(sku.getWeight()).setVolume(sku.getVolume()); |
| 61 | 61 | // spu 信息 |
| 62 | 62 | orderItem.setSpuName(spu.getName()).setCategoryId(spu.getCategoryId()) |
| 63 | - .setDeliveryTemplateId(spu.getDeliveryTemplateId()) | |
| 63 | + .setDeliveryTypes(spu.getDeliveryTypes()).setDeliveryTemplateId(spu.getDeliveryTemplateId()) | |
| 64 | 64 | .setGivePoint(spu.getGiveIntegral()).setUsePoint(0); |
| 65 | 65 | if (StrUtil.isBlank(orderItem.getPicUrl())) { |
| 66 | 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 | 3 | import cn.hutool.core.collection.CollUtil; |
| 4 | 4 | import cn.hutool.core.util.ObjectUtil; |
| 5 | 5 | import cn.hutool.core.util.StrUtil; |
| 6 | +import cn.iocoder.yudao.framework.common.util.number.MoneyUtils; | |
| 6 | 7 | import cn.iocoder.yudao.module.promotion.api.reward.RewardActivityApi; |
| 7 | 8 | import cn.iocoder.yudao.module.promotion.api.reward.dto.RewardActivityMatchRespDTO; |
| 8 | 9 | import cn.iocoder.yudao.module.promotion.enums.common.PromotionConditionTypeEnum; |
| 10 | +import cn.iocoder.yudao.module.promotion.enums.common.PromotionProductScopeEnum; | |
| 9 | 11 | import cn.iocoder.yudao.module.promotion.enums.common.PromotionTypeEnum; |
| 10 | 12 | import cn.iocoder.yudao.module.trade.enums.order.TradeOrderTypeEnum; |
| 11 | 13 | import cn.iocoder.yudao.module.trade.service.price.bo.TradePriceCalculateReqBO; |
| 12 | 14 | import cn.iocoder.yudao.module.trade.service.price.bo.TradePriceCalculateRespBO; |
| 15 | +import jakarta.annotation.Resource; | |
| 13 | 16 | import org.springframework.core.annotation.Order; |
| 14 | 17 | import org.springframework.stereotype.Component; |
| 15 | 18 | |
| 16 | -import jakarta.annotation.Resource; | |
| 19 | +import java.util.ArrayList; | |
| 20 | +import java.util.Comparator; | |
| 17 | 21 | import java.util.List; |
| 22 | +import java.util.Map; | |
| 18 | 23 | |
| 19 | 24 | import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertSet; |
| 20 | 25 | import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.filterList; |
| 21 | 26 | import static cn.iocoder.yudao.module.trade.service.price.calculator.TradePriceCalculatorHelper.formatPrice; |
| 22 | 27 | |
| 28 | +// TODO @puhui999:相关的单测,建议改一改 | |
| 29 | + | |
| 23 | 30 | /** |
| 24 | 31 | * 满减送活动的 {@link TradePriceCalculator} 实现类 |
| 25 | 32 | * |
| ... | ... | @@ -52,7 +59,7 @@ public class TradeRewardActivityPriceCalculator implements TradePriceCalculator |
| 52 | 59 | private void calculate(TradePriceCalculateReqBO param, TradePriceCalculateRespBO result, |
| 53 | 60 | RewardActivityMatchRespDTO rewardActivity) { |
| 54 | 61 | // 1.1 获得满减送的订单项(商品)列表 |
| 55 | - List<TradePriceCalculateRespBO.OrderItem> orderItems = filterMatchCouponOrderItems(result, rewardActivity); | |
| 62 | + List<TradePriceCalculateRespBO.OrderItem> orderItems = filterMatchActivityOrderItems(result, rewardActivity); | |
| 56 | 63 | if (CollUtil.isEmpty(orderItems)) { |
| 57 | 64 | return; |
| 58 | 65 | } |
| ... | ... | @@ -61,7 +68,7 @@ public class TradeRewardActivityPriceCalculator implements TradePriceCalculator |
| 61 | 68 | if (rule == null) { |
| 62 | 69 | TradePriceCalculatorHelper.addNotMatchPromotion(result, orderItems, |
| 63 | 70 | rewardActivity.getId(), rewardActivity.getName(), PromotionTypeEnum.REWARD_ACTIVITY.getType(), |
| 64 | - getRewardActivityNotMeetTip(rewardActivity)); | |
| 71 | + getRewardActivityNotMeetTip(rewardActivity, orderItems)); | |
| 65 | 72 | return; |
| 66 | 73 | } |
| 67 | 74 | |
| ... | ... | @@ -84,6 +91,36 @@ public class TradeRewardActivityPriceCalculator implements TradePriceCalculator |
| 84 | 91 | TradePriceCalculatorHelper.recountPayPrice(orderItem); |
| 85 | 92 | } |
| 86 | 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 | 130 | * @param rewardActivity 满减送活动 |
| 94 | 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 | 179 | } |
| 130 | 180 | |
| 131 | 181 | /** |
| 132 | - * 获得满减送活动部匹配时的提示 | |
| 182 | + * 获得满减送活动不匹配时的提示 | |
| 133 | 183 | * |
| 134 | 184 | * @param rewardActivity 满减送活动 |
| 135 | 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 | 4 | import cn.iocoder.yudao.module.promotion.api.reward.RewardActivityApi; |
| 5 | 5 | import cn.iocoder.yudao.module.promotion.api.reward.dto.RewardActivityMatchRespDTO; |
| 6 | 6 | import cn.iocoder.yudao.module.promotion.enums.common.PromotionConditionTypeEnum; |
| 7 | +import cn.iocoder.yudao.module.promotion.enums.common.PromotionProductScopeEnum; | |
| 7 | 8 | import cn.iocoder.yudao.module.promotion.enums.common.PromotionTypeEnum; |
| 8 | 9 | import cn.iocoder.yudao.module.trade.enums.order.TradeOrderTypeEnum; |
| 9 | 10 | import cn.iocoder.yudao.module.trade.service.price.bo.TradePriceCalculateReqBO; |
| ... | ... | @@ -13,6 +14,7 @@ import org.mockito.InjectMocks; |
| 13 | 14 | import org.mockito.Mock; |
| 14 | 15 | |
| 15 | 16 | import java.util.ArrayList; |
| 17 | +import java.util.LinkedHashMap; | |
| 16 | 18 | |
| 17 | 19 | import static cn.iocoder.yudao.framework.common.util.collection.SetUtils.asSet; |
| 18 | 20 | import static cn.iocoder.yudao.framework.test.core.util.RandomUtils.randomPojo; |
| ... | ... | @@ -47,7 +49,7 @@ public class TradeRewardActivityPriceCalculatorTest extends BaseMockitoUnitTest |
| 47 | 49 | TradePriceCalculateRespBO result = new TradePriceCalculateRespBO() |
| 48 | 50 | .setType(TradeOrderTypeEnum.NORMAL.getType()) |
| 49 | 51 | .setPrice(new TradePriceCalculateRespBO.Price()) |
| 50 | - .setPromotions(new ArrayList<>()) | |
| 52 | + .setPromotions(new ArrayList<>()).setGiveCouponTemplateCounts(new LinkedHashMap<>()) | |
| 51 | 53 | .setItems(asList( |
| 52 | 54 | new TradePriceCalculateRespBO.OrderItem().setSkuId(10L).setCount(2).setSelected(true) |
| 53 | 55 | .setPrice(100).setSpuId(1L), |
| ... | ... | @@ -60,16 +62,22 @@ public class TradeRewardActivityPriceCalculatorTest extends BaseMockitoUnitTest |
| 60 | 62 | TradePriceCalculatorHelper.recountPayPrice(result.getItems()); |
| 61 | 63 | TradePriceCalculatorHelper.recountAllPrice(result); |
| 62 | 64 | |
| 63 | - // mock 方法(限时折扣 DiscountActivity 信息) | |
| 65 | + // mock 方法(满减送 RewardActivity 信息) | |
| 64 | 66 | when(rewardActivityApi.getMatchRewardActivityList(eq(asSet(1L, 2L, 3L)))).thenReturn(asList( |
| 65 | 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 | 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 | 102 | assertEquals(orderItem01.getCouponPrice(), 0); |
| 95 | 103 | assertEquals(orderItem01.getPointPrice(), 0); |
| 96 | 104 | assertEquals(orderItem01.getPayPrice(), 160); |
| 105 | + assertEquals(orderItem01.getGivePoint(), 0); | |
| 97 | 106 | // 断言:SKU 2 |
| 98 | 107 | TradePriceCalculateRespBO.OrderItem orderItem02 = result.getItems().get(1); |
| 99 | 108 | assertEquals(orderItem02.getSkuId(), 20L); |
| ... | ... | @@ -104,6 +113,7 @@ public class TradeRewardActivityPriceCalculatorTest extends BaseMockitoUnitTest |
| 104 | 113 | assertEquals(orderItem02.getCouponPrice(), 0); |
| 105 | 114 | assertEquals(orderItem02.getPointPrice(), 0); |
| 106 | 115 | assertEquals(orderItem02.getPayPrice(), 120); |
| 116 | + assertEquals(orderItem02.getGivePoint(), 0); | |
| 107 | 117 | // 断言:SKU 3 |
| 108 | 118 | TradePriceCalculateRespBO.OrderItem orderItem03 = result.getItems().get(2); |
| 109 | 119 | assertEquals(orderItem03.getSkuId(), 30L); |
| ... | ... | @@ -114,6 +124,7 @@ public class TradeRewardActivityPriceCalculatorTest extends BaseMockitoUnitTest |
| 114 | 124 | assertEquals(orderItem03.getCouponPrice(), 0); |
| 115 | 125 | assertEquals(orderItem03.getPointPrice(), 0); |
| 116 | 126 | assertEquals(orderItem03.getPayPrice(), 60); |
| 127 | + assertEquals(orderItem03.getGivePoint(), 100); | |
| 117 | 128 | // 断言:Promotion 部分(第一个) |
| 118 | 129 | assertEquals(result.getPromotions().size(), 2); |
| 119 | 130 | TradePriceCalculateRespBO.Promotion promotion01 = result.getPromotions().get(0); |
| ... | ... | @@ -175,7 +186,7 @@ public class TradeRewardActivityPriceCalculatorTest extends BaseMockitoUnitTest |
| 175 | 186 | // mock 方法(限时折扣 DiscountActivity 信息) |
| 176 | 187 | when(rewardActivityApi.getMatchRewardActivityList(eq(asSet(1L, 2L)))).thenReturn(singletonList( |
| 177 | 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 | 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 | 48 | "give_point" int NULL, |
| 49 | 49 | "refund_point" int NULL, |
| 50 | 50 | "vip_price" int NULL, |
| 51 | + "give_coupons_map" varchar NULL, | |
| 51 | 52 | "seckill_activity_id" long NULL, |
| 52 | 53 | "bargain_activity_id" long NULL, |
| 53 | 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 | 18 | import io.swagger.v3.oas.annotations.Operation; |
| 19 | 19 | import io.swagger.v3.oas.annotations.Parameter; |
| 20 | 20 | import io.swagger.v3.oas.annotations.tags.Tag; |
| 21 | +import jakarta.annotation.Resource; | |
| 22 | +import jakarta.validation.Valid; | |
| 21 | 23 | import org.springframework.security.access.prepost.PreAuthorize; |
| 22 | 24 | import org.springframework.validation.annotation.Validated; |
| 23 | 25 | import org.springframework.web.bind.annotation.*; |
| 24 | 26 | |
| 25 | -import jakarta.annotation.Resource; | |
| 26 | -import jakarta.validation.Valid; | |
| 27 | 27 | import java.util.Collection; |
| 28 | 28 | import java.util.List; |
| 29 | 29 | import java.util.Objects; |
| ... | ... | @@ -76,14 +76,6 @@ public class MemberUserController { |
| 76 | 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 | 79 | @GetMapping("/get") |
| 88 | 80 | @Operation(summary = "获得会员用户") |
| 89 | 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 | 18 | RECHARGE(1, "充值"), |
| 19 | 19 | RECHARGE_REFUND(2, "充值退款"), |
| 20 | 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 | 4 | import cn.iocoder.yudao.framework.common.pojo.PageResult; |
| 5 | 5 | import cn.iocoder.yudao.module.pay.controller.admin.wallet.vo.wallet.PayWalletPageReqVO; |
| 6 | 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 | 8 | import cn.iocoder.yudao.module.pay.controller.admin.wallet.vo.wallet.PayWalletUserReqVO; |
| 8 | 9 | import cn.iocoder.yudao.module.pay.convert.wallet.PayWalletConvert; |
| 9 | 10 | import cn.iocoder.yudao.module.pay.dal.dataobject.wallet.PayWalletDO; |
| 11 | +import cn.iocoder.yudao.module.pay.enums.wallet.PayWalletBizTypeEnum; | |
| 10 | 12 | import cn.iocoder.yudao.module.pay.service.wallet.PayWalletService; |
| 11 | 13 | import io.swagger.v3.oas.annotations.Operation; |
| 12 | 14 | import io.swagger.v3.oas.annotations.tags.Tag; |
| ... | ... | @@ -15,12 +17,12 @@ import jakarta.validation.Valid; |
| 15 | 17 | import lombok.extern.slf4j.Slf4j; |
| 16 | 18 | import org.springframework.security.access.prepost.PreAuthorize; |
| 17 | 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 | 22 | import static cn.iocoder.yudao.framework.common.enums.UserTypeEnum.MEMBER; |
| 23 | +import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception; | |
| 23 | 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 | 27 | @Tag(name = "管理后台 - 用户钱包") |
| 26 | 28 | @RestController |
| ... | ... | @@ -48,4 +50,21 @@ public class PayWalletController { |
| 48 | 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 | import cn.iocoder.yudao.module.pay.service.order.PayOrderService; |
| 13 | 13 | import cn.iocoder.yudao.module.pay.service.refund.PayRefundService; |
| 14 | 14 | import cn.iocoder.yudao.module.pay.service.wallet.bo.WalletTransactionCreateReqBO; |
| 15 | +import jakarta.annotation.Resource; | |
| 15 | 16 | import lombok.extern.slf4j.Slf4j; |
| 16 | 17 | import org.springframework.context.annotation.Lazy; |
| 17 | 18 | import org.springframework.stereotype.Service; |
| 18 | 19 | import org.springframework.transaction.annotation.Transactional; |
| 19 | 20 | |
| 20 | -import jakarta.annotation.Resource; | |
| 21 | 21 | import java.time.LocalDateTime; |
| 22 | 22 | |
| 23 | 23 | import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception; |
| ... | ... | @@ -176,6 +176,9 @@ public class PayWalletServiceImpl implements PayWalletService { |
| 176 | 176 | walletMapper.updateWhenRecharge(payWallet.getId(), price); |
| 177 | 177 | break; |
| 178 | 178 | } |
| 179 | + case UPDATE_BALANCE: // 更新余额 | |
| 180 | + walletMapper.updateWhenRecharge(payWallet.getId(), price); | |
| 181 | + break; | |
| 179 | 182 | default: { |
| 180 | 183 | // TODO 其它类型待实现 |
| 181 | 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 | 6 | import cn.iocoder.yudao.module.system.service.sms.SmsSendService; |
| 7 | 7 | import io.swagger.v3.oas.annotations.Operation; |
| 8 | 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 | 11 | import jakarta.annotation.Resource; |
| 14 | 12 | import jakarta.annotation.security.PermitAll; |
| ... | ... | @@ -26,7 +24,7 @@ public class SmsCallbackController { |
| 26 | 24 | |
| 27 | 25 | @PostMapping("/aliyun") |
| 28 | 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 | 28 | public CommonResult<Boolean> receiveAliyunSmsStatus(HttpServletRequest request) throws Throwable { |
| 31 | 29 | String text = ServletUtils.getBody(request); |
| 32 | 30 | smsSendService.receiveSmsStatus(SmsChannelEnum.ALIYUN.getCode(), text); |
| ... | ... | @@ -35,7 +33,7 @@ public class SmsCallbackController { |
| 35 | 33 | |
| 36 | 34 | @PostMapping("/tencent") |
| 37 | 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 | 37 | public CommonResult<Boolean> receiveTencentSmsStatus(HttpServletRequest request) throws Throwable { |
| 40 | 38 | String text = ServletUtils.getBody(request); |
| 41 | 39 | smsSendService.receiveSmsStatus(SmsChannelEnum.TENCENT.getCode(), text); |
| ... | ... | @@ -46,10 +44,17 @@ public class SmsCallbackController { |
| 46 | 44 | @PostMapping("/huawei") |
| 47 | 45 | @PermitAll |
| 48 | 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 | 57 | return success(true); |
| 53 | 58 | } |
| 54 | 59 | |
| 55 | 60 | -} |
| 61 | +} | |
| 56 | 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 | 30 | * 创建短信 Client |
| 31 | 31 | * |
| 32 | 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 | 26 | * 初始化 |
| 27 | 27 | */ |
| 28 | 28 | public final void init() { |
| 29 | - doInit(); | |
| 30 | 29 | log.debug("[init][配置({}) 初始化完成]", properties); |
| 31 | 30 | } |
| 32 | 31 | |
| 33 | - /** | |
| 34 | - * 自定义初始化 | |
| 35 | - */ | |
| 36 | - protected abstract void doInit(); | |
| 37 | - | |
| 38 | 32 | public final void refresh(SmsChannelProperties properties) { |
| 39 | 33 | // 判断是否更新 |
| 40 | 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 | 51 | } |
| 52 | 52 | |
| 53 | 53 | @Override |
| 54 | - protected void doInit() { | |
| 55 | - } | |
| 56 | - | |
| 57 | - @Override | |
| 58 | 54 | public SmsSendRespDTO sendSms(Long sendLogId, String mobile, String apiTemplateId, |
| 59 | 55 | List<KeyValue<String, Object>> templateParams) throws Throwable { |
| 60 | 56 | Assert.notBlank(properties.getSignature(), "短信签名不能为空"); |
| ... | ... | @@ -80,7 +76,7 @@ public class AliyunSmsClient extends AbstractSmsClient { |
| 80 | 76 | @Override |
| 81 | 77 | public List<SmsReceiveRespDTO> parseSmsReceiveStatus(String text) { |
| 82 | 78 | JSONArray statuses = JSONUtil.parseArray(text); |
| 83 | - // 字段参考 | |
| 79 | + // 字段参考 https://help.aliyun.com/zh/sms/developer-reference/smsreport-2 | |
| 84 | 80 | return convertList(statuses, status -> { |
| 85 | 81 | JSONObject statusObj = (JSONObject) status; |
| 86 | 82 | return new SmsReceiveRespDTO() |
| ... | ... | @@ -166,7 +162,8 @@ public class AliyunSmsClient extends AbstractSmsClient { |
| 166 | 162 | String hashedRequestBody = DigestUtil.sha256Hex(requestBody); |
| 167 | 163 | |
| 168 | 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 | 167 | String hashedCanonicalRequest = DigestUtil.sha256Hex(canonicalRequest); |
| 171 | 168 | String stringToSign = "ACS3-HMAC-SHA256" + "\n" + hashedCanonicalRequest; |
| 172 | 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 | 37 | } |
| 38 | 38 | |
| 39 | 39 | @Override |
| 40 | - protected void doInit() { | |
| 41 | - } | |
| 42 | - | |
| 43 | - @Override | |
| 44 | 40 | public SmsSendRespDTO sendSms(Long sendLogId, String mobile, |
| 45 | 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 | 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 | 5 | import cn.hutool.core.lang.Assert; |
| 6 | +import cn.hutool.core.util.CharsetUtil; | |
| 4 | 7 | import cn.hutool.core.util.StrUtil; |
| 5 | - | |
| 6 | 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 | 10 | import cn.hutool.json.JSONObject; |
| 10 | 11 | import cn.hutool.json.JSONUtil; |
| 11 | 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 | 14 | import cn.iocoder.yudao.framework.common.util.json.JsonUtils; |
| 14 | 15 | import cn.iocoder.yudao.module.system.framework.sms.core.client.dto.SmsReceiveRespDTO; |
| 15 | 16 | import cn.iocoder.yudao.module.system.framework.sms.core.client.dto.SmsSendRespDTO; |
| 16 | 17 | import cn.iocoder.yudao.module.system.framework.sms.core.client.dto.SmsTemplateRespDTO; |
| 17 | 18 | import cn.iocoder.yudao.module.system.framework.sms.core.enums.SmsTemplateAuditStatusEnum; |
| 18 | 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 | 20 | import lombok.extern.slf4j.Slf4j; |
| 24 | 21 | |
| 25 | 22 | import java.io.UnsupportedEncodingException; |
| 26 | 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 | 26 | import java.time.LocalDateTime; |
| 27 | +import java.time.ZoneId; | |
| 28 | +import java.util.*; | |
| 32 | 29 | |
| 33 | 30 | import static cn.hutool.crypto.digest.DigestUtil.sha256Hex; |
| 34 | 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 | 39 | @Slf4j |
| 46 | 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 | 48 | public HuaweiSmsClient(SmsChannelProperties properties) { |
| 60 | 49 | super(properties); |
| 61 | 50 | Assert.notEmpty(properties.getApiKey(), "apiKey 不能为空"); |
| 62 | 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 | 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 | 150 | @Override |
| 170 | 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 | 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 | 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 | 59 | } |
| 60 | 60 | |
| 61 | 61 | @Override |
| 62 | - public void createOrUpdateSmsClient(SmsChannelProperties properties) { | |
| 62 | + public SmsClient createOrUpdateSmsClient(SmsChannelProperties properties) { | |
| 63 | 63 | AbstractSmsClient client = channelIdClients.get(properties.getId()); |
| 64 | 64 | if (client == null) { |
| 65 | 65 | client = this.createSmsClient(properties); |
| ... | ... | @@ -68,6 +68,7 @@ public class SmsClientFactoryImpl implements SmsClientFactory { |
| 68 | 68 | } else { |
| 69 | 69 | client.refresh(properties); |
| 70 | 70 | } |
| 71 | + return client; | |
| 71 | 72 | } |
| 72 | 73 | |
| 73 | 74 | private AbstractSmsClient createSmsClient(SmsChannelProperties properties) { |
| ... | ... | @@ -79,6 +80,7 @@ public class SmsClientFactoryImpl implements SmsClientFactory { |
| 79 | 80 | case DEBUG_DING_TALK: return new DebugDingTalkSmsClient(properties); |
| 80 | 81 | case TENCENT: return new TencentSmsClient(properties); |
| 81 | 82 | case HUAWEI: return new HuaweiSmsClient(properties); |
| 83 | + case QINIU: return new QiniuSmsClient(properties); | |
| 82 | 84 | } |
| 83 | 85 | // 创建失败,错误日志 + 抛出异常 |
| 84 | 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 | 1 | package cn.iocoder.yudao.module.system.framework.sms.core.client.impl; |
| 2 | 2 | |
| 3 | +import cn.hutool.core.date.format.FastDateFormat; | |
| 3 | 4 | import cn.hutool.core.lang.Assert; |
| 5 | +import cn.hutool.core.util.HexUtil; | |
| 4 | 6 | import cn.hutool.core.util.StrUtil; |
| 7 | +import cn.hutool.crypto.digest.DigestUtil; | |
| 8 | +import cn.hutool.crypto.digest.HmacAlgorithm; | |
| 5 | 9 | import cn.hutool.json.JSONArray; |
| 6 | 10 | import cn.hutool.json.JSONObject; |
| 7 | 11 | import cn.hutool.json.JSONUtil; |
| ... | ... | @@ -14,12 +18,8 @@ import cn.iocoder.yudao.module.system.framework.sms.core.client.dto.SmsTemplateR |
| 14 | 18 | import cn.iocoder.yudao.module.system.framework.sms.core.enums.SmsTemplateAuditStatusEnum; |
| 15 | 19 | import cn.iocoder.yudao.module.system.framework.sms.core.property.SmsChannelProperties; |
| 16 | 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 | 22 | import java.nio.charset.StandardCharsets; |
| 22 | -import java.text.SimpleDateFormat; | |
| 23 | 23 | import java.util.*; |
| 24 | 24 | |
| 25 | 25 | import static cn.hutool.crypto.digest.DigestUtil.sha256Hex; |
| ... | ... | @@ -34,6 +34,7 @@ import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils. |
| 34 | 34 | */ |
| 35 | 35 | public class TencentSmsClient extends AbstractSmsClient { |
| 36 | 36 | |
| 37 | + private static final String HOST = "sms.tencentcloudapi.com"; | |
| 37 | 38 | private static final String VERSION = "2021-01-11"; |
| 38 | 39 | private static final String REGION = "ap-guangzhou"; |
| 39 | 40 | |
| ... | ... | @@ -56,10 +57,6 @@ public class TencentSmsClient extends AbstractSmsClient { |
| 56 | 57 | validateSdkAppId(properties); |
| 57 | 58 | } |
| 58 | 59 | |
| 59 | - @Override | |
| 60 | - protected void doInit() { | |
| 61 | - } | |
| 62 | - | |
| 63 | 60 | /** |
| 64 | 61 | * 参数校验腾讯云的 SDK AppId |
| 65 | 62 | * |
| ... | ... | @@ -93,7 +90,7 @@ public class TencentSmsClient extends AbstractSmsClient { |
| 93 | 90 | body.put("PhoneNumberSet", new String[]{mobile}); |
| 94 | 91 | body.put("SmsSdkAppId", getSdkAppId()); |
| 95 | 92 | body.put("SignName", properties.getSignature()); |
| 96 | - body.put("TemplateId",apiTemplateId); | |
| 93 | + body.put("TemplateId", apiTemplateId); | |
| 97 | 94 | body.put("TemplateParamSet", ArrayUtils.toArray(templateParams, param -> String.valueOf(param.getValue()))); |
| 98 | 95 | JSONObject response = request("SendSms", body); |
| 99 | 96 | |
| ... | ... | @@ -106,11 +103,11 @@ public class TencentSmsClient extends AbstractSmsClient { |
| 106 | 103 | .setApiCode(error.getStr("Code")) |
| 107 | 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 | 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 | 113 | @Override |
| ... | ... | @@ -137,14 +134,13 @@ public class TencentSmsClient extends AbstractSmsClient { |
| 137 | 134 | body.put("TemplateIdSet", new Integer[]{Integer.valueOf(apiTemplateId)}); |
| 138 | 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 | 146 | @VisibleForTesting |
| ... | ... | @@ -166,64 +162,40 @@ public class TencentSmsClient extends AbstractSmsClient { |
| 166 | 162 | * @param body 请求参数 |
| 167 | 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 | 167 | Map<String, String> headers = new HashMap<>(); |
| 209 | - headers.put("Authorization", authorization); | |
| 210 | 168 | headers.put("Content-Type", "application/json; charset=utf-8"); |
| 211 | - headers.put("Host", host); | |
| 169 | + headers.put("Host", HOST); | |
| 212 | 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 | 174 | headers.put("X-TC-Version", VERSION); |
| 215 | 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 | 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 | 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
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 | 56 | private AdminUserService adminUserService; |
| 57 | 57 | |
| 58 | 58 | @Override |
| 59 | - @Transactional | |
| 59 | + @Transactional(rollbackFor = Exception.class) | |
| 60 | 60 | public OAuth2AccessTokenDO createAccessToken(Long userId, Integer userType, String clientId, List<String> scopes) { |
| 61 | 61 | OAuth2ClientDO clientDO = oauth2ClientService.validOAuthClientFromCache(clientId); |
| 62 | 62 | // 创建刷新令牌 |
| ... | ... | @@ -66,6 +66,7 @@ public class OAuth2TokenServiceImpl implements OAuth2TokenService { |
| 66 | 66 | } |
| 67 | 67 | |
| 68 | 68 | @Override |
| 69 | + @Transactional(rollbackFor = Exception.class) | |
| 69 | 70 | public OAuth2AccessTokenDO refreshAccessToken(String refreshToken, String clientId) { |
| 70 | 71 | // 查询访问令牌 |
| 71 | 72 | OAuth2RefreshTokenDO refreshTokenDO = oauth2RefreshTokenMapper.selectByRefreshToken(refreshToken); |
| ... | ... | @@ -82,7 +83,7 @@ public class OAuth2TokenServiceImpl implements OAuth2TokenService { |
| 82 | 83 | // 移除相关的访问令牌 |
| 83 | 84 | List<OAuth2AccessTokenDO> accessTokenDOs = oauth2AccessTokenMapper.selectListByRefreshToken(refreshToken); |
| 84 | 85 | if (CollUtil.isNotEmpty(accessTokenDOs)) { |
| 85 | - oauth2AccessTokenMapper.deleteBatchIds(convertSet(accessTokenDOs, OAuth2AccessTokenDO::getId)); | |
| 86 | + oauth2AccessTokenMapper.deleteByIds(convertSet(accessTokenDOs, OAuth2AccessTokenDO::getId)); | |
| 86 | 87 | oauth2AccessTokenRedisDAO.deleteList(convertSet(accessTokenDOs, OAuth2AccessTokenDO::getAccessToken)); |
| 87 | 88 | } |
| 88 | 89 | |
| ... | ... | @@ -126,6 +127,7 @@ public class OAuth2TokenServiceImpl implements OAuth2TokenService { |
| 126 | 127 | } |
| 127 | 128 | |
| 128 | 129 | @Override |
| 130 | + @Transactional(rollbackFor = Exception.class) | |
| 129 | 131 | public OAuth2AccessTokenDO removeAccessToken(String accessToken) { |
| 130 | 132 | // 删除访问令牌 |
| 131 | 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 | 1 | package cn.iocoder.yudao.module.system.service.sms; |
| 2 | 2 | |
| 3 | -import cn.hutool.core.util.StrUtil; | |
| 4 | 3 | import cn.iocoder.yudao.framework.common.pojo.PageResult; |
| 5 | 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 | 5 | import cn.iocoder.yudao.module.system.controller.admin.sms.vo.channel.SmsChannelPageReqVO; |
| 10 | 6 | import cn.iocoder.yudao.module.system.controller.admin.sms.vo.channel.SmsChannelSaveReqVO; |
| 11 | 7 | import cn.iocoder.yudao.module.system.dal.dataobject.sms.SmsChannelDO; |
| 12 | 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 | 13 | import lombok.extern.slf4j.Slf4j; |
| 17 | 14 | import org.springframework.stereotype.Service; |
| 18 | 15 | |
| 19 | -import jakarta.annotation.Resource; | |
| 20 | -import java.time.Duration; | |
| 21 | 16 | import java.util.List; |
| 22 | 17 | |
| 23 | 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 | 19 | import static cn.iocoder.yudao.module.system.enums.ErrorCodeConstants.SMS_CHANNEL_HAS_CHILDREN; |
| 26 | 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 | 28 | @Slf4j |
| 35 | 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 | 31 | @Resource |
| 78 | 32 | private SmsClientFactory smsClientFactory; |
| 79 | 33 | |
| ... | ... | @@ -93,41 +47,22 @@ public class SmsChannelServiceImpl implements SmsChannelService { |
| 93 | 47 | @Override |
| 94 | 48 | public void updateSmsChannel(SmsChannelSaveReqVO updateReqVO) { |
| 95 | 49 | // 校验存在 |
| 96 | - SmsChannelDO channel = validateSmsChannelExists(updateReqVO.getId()); | |
| 50 | + validateSmsChannelExists(updateReqVO.getId()); | |
| 97 | 51 | // 更新 |
| 98 | 52 | SmsChannelDO updateObj = BeanUtils.toBean(updateReqVO, SmsChannelDO.class); |
| 99 | 53 | smsChannelMapper.updateById(updateObj); |
| 100 | - | |
| 101 | - // 清空缓存 | |
| 102 | - clearCache(updateReqVO.getId(), channel.getCode()); | |
| 103 | 54 | } |
| 104 | 55 | |
| 105 | 56 | @Override |
| 106 | 57 | public void deleteSmsChannel(Long id) { |
| 107 | 58 | // 校验存在 |
| 108 | - SmsChannelDO channel = validateSmsChannelExists(id); | |
| 59 | + validateSmsChannelExists(id); | |
| 109 | 60 | // 校验是否有在使用该账号的模版 |
| 110 | 61 | if (smsTemplateService.getSmsTemplateCountByChannelId(id) > 0) { |
| 111 | 62 | throw exception(SMS_CHANNEL_HAS_CHILDREN); |
| 112 | 63 | } |
| 113 | 64 | // 删除 |
| 114 | 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 | 68 | private SmsChannelDO validateSmsChannelExists(Long id) { |
| ... | ... | @@ -155,12 +90,14 @@ public class SmsChannelServiceImpl implements SmsChannelService { |
| 155 | 90 | |
| 156 | 91 | @Override |
| 157 | 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 | 98 | @Override |
| 162 | 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 | 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 | 2 | |
| 3 | 3 | import cn.hutool.core.collection.ListUtil; |
| 4 | 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 | 6 | import cn.iocoder.yudao.module.system.framework.sms.core.client.dto.SmsSendRespDTO; |
| 7 | 7 | import cn.iocoder.yudao.module.system.framework.sms.core.client.dto.SmsTemplateRespDTO; |
| 8 | 8 | import cn.iocoder.yudao.module.system.framework.sms.core.property.SmsChannelProperties; |
| ... | ... | @@ -12,7 +12,7 @@ import org.junit.jupiter.api.Test; |
| 12 | 12 | import java.util.List; |
| 13 | 13 | |
| 14 | 14 | /** |
| 15 | - * 各种 {@link SmsClientTests 集成测试 | |
| 15 | + * 各种 {@link SmsClient} 的集成测试 | |
| 16 | 16 | * |
| 17 | 17 | * @author 芋道源码 |
| 18 | 18 | */ |
| ... | ... | @@ -24,8 +24,8 @@ public class SmsClientTests { |
| 24 | 24 | @Disabled |
| 25 | 25 | public void testAliyunSmsClient_getSmsTemplate() throws Throwable { |
| 26 | 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 | 29 | AliyunSmsClient client = new AliyunSmsClient(properties); |
| 30 | 30 | // 准备参数 |
| 31 | 31 | String apiTemplateId = "SMS_207945135"; |
| ... | ... | @@ -39,9 +39,9 @@ public class SmsClientTests { |
| 39 | 39 | @Disabled |
| 40 | 40 | public void testAliyunSmsClient_sendSms() throws Throwable { |
| 41 | 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 | 45 | AliyunSmsClient client = new AliyunSmsClient(properties); |
| 46 | 46 | // 准备参数 |
| 47 | 47 | Long sendLogId = System.currentTimeMillis(); |
| ... | ... | @@ -53,49 +53,21 @@ public class SmsClientTests { |
| 53 | 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 | 58 | @Test |
| 88 | 59 | @Disabled |
| 89 | 60 | public void testTencentSmsClient_sendSms() throws Throwable { |
| 61 | + String sdkAppId = "1400500458"; | |
| 90 | 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 | 65 | .setSignature("芋道源码"); |
| 94 | 66 | TencentSmsClient client = new TencentSmsClient(properties); |
| 95 | 67 | // 准备参数 |
| 96 | 68 | Long sendLogId = System.currentTimeMillis(); |
| 97 | 69 | String mobile = "15601691323"; |
| 98 | - String apiTemplateId = "2136358"; | |
| 70 | + String apiTemplateId = "358212"; | |
| 99 | 71 | // 调用 |
| 100 | 72 | SmsSendRespDTO sendRespDTO = client.sendSms(sendLogId, mobile, apiTemplateId, ListUtil.of(new KeyValue<>("code", "1024"))); |
| 101 | 73 | // 打印结果 |
| ... | ... | @@ -105,13 +77,14 @@ public class SmsClientTests { |
| 105 | 77 | @Test |
| 106 | 78 | @Disabled |
| 107 | 79 | public void testTencentSmsClient_getSmsTemplate() throws Throwable { |
| 80 | + String sdkAppId = "1400500458"; | |
| 108 | 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 | 84 | .setSignature("芋道源码"); |
| 112 | 85 | TencentSmsClient client = new TencentSmsClient(properties); |
| 113 | 86 | // 准备参数 |
| 114 | - String apiTemplateId = "2136358"; | |
| 87 | + String apiTemplateId = "358212"; | |
| 115 | 88 | // 调用 |
| 116 | 89 | SmsTemplateRespDTO template = client.getSmsTemplate(apiTemplateId); |
| 117 | 90 | // 打印结果 |
| ... | ... | @@ -123,15 +96,16 @@ public class SmsClientTests { |
| 123 | 96 | @Test |
| 124 | 97 | @Disabled |
| 125 | 98 | public void testHuaweiSmsClient_sendSms() throws Throwable { |
| 99 | + String sender = "x8824060312575"; | |
| 126 | 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 | 103 | .setSignature("runpu"); |
| 130 | 104 | HuaweiSmsClient client = new HuaweiSmsClient(properties); |
| 131 | 105 | // 准备参数 |
| 132 | 106 | Long sendLogId = System.currentTimeMillis(); |
| 133 | - String mobile = "15601691323"; | |
| 134 | - String apiTemplateId = "xx test01"; | |
| 107 | + String mobile = "17321315478"; | |
| 108 | + String apiTemplateId = "3644cdab863546a3b718d488659a99ef"; | |
| 135 | 109 | List<KeyValue<String, Object>> templateParams = ListUtil.of(new KeyValue<>("code", "1024")); |
| 136 | 110 | // 调用 |
| 137 | 111 | SmsSendRespDTO smsSendRespDTO = client.sendSms(sendLogId, mobile, apiTemplateId, templateParams); |
| ... | ... | @@ -139,5 +113,39 @@ public class SmsClientTests { |
| 139 | 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 | 78 | } |
| 79 | 79 | |
| 80 | 80 | @Test |
| 81 | - public void testDoSendSms_fail() throws Throwable { | |
| 81 | + public void testDoSendSms_fail_01() throws Throwable { | |
| 82 | 82 | try (MockedStatic<HttpUtils> httpUtilsMockedStatic = mockStatic(HttpUtils.class)) { |
| 83 | 83 | // 准备参数 |
| 84 | 84 | Long sendLogId = randomLongId(); |
| ... | ... | @@ -118,6 +118,31 @@ public class TencentSmsClientTest extends BaseMockitoUnitTest { |
| 118 | 118 | } |
| 119 | 119 | |
| 120 | 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 | 146 | public void testParseSmsReceiveStatus() { |
| 122 | 147 | // 准备参数 |
| 123 | 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 | 57 | // 校验记录的属性是否正确 |
| 58 | 58 | SmsChannelDO smsChannel = smsChannelMapper.selectById(smsChannelId); |
| 59 | 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 | 62 | @Test |
| ... | ... | @@ -79,9 +76,6 @@ public class SmsChannelServiceTest extends BaseDbUnitTest { |
| 79 | 76 | // 校验是否更新正确 |
| 80 | 77 | SmsChannelDO smsChannel = smsChannelMapper.selectById(reqVO.getId()); // 获取最新的 |
| 81 | 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 | 81 | @Test |
| ... | ... | @@ -105,9 +99,6 @@ public class SmsChannelServiceTest extends BaseDbUnitTest { |
| 105 | 99 | smsChannelService.deleteSmsChannel(id); |
| 106 | 100 | // 校验数据不存在了 |
| 107 | 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 | 104 | @Test |
| ... | ... | @@ -196,29 +187,23 @@ public class SmsChannelServiceTest extends BaseDbUnitTest { |
| 196 | 187 | // mock 数据 |
| 197 | 188 | SmsChannelDO channel = randomPojo(SmsChannelDO.class); |
| 198 | 189 | smsChannelMapper.insert(channel); |
| 199 | - // mock 参数 | |
| 190 | + // 准备参数 | |
| 200 | 191 | Long id = channel.getId(); |
| 201 | 192 | // mock 方法 |
| 202 | 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 | 198 | SmsClient client = smsChannelService.getSmsClient(id); |
| 207 | 199 | // 断言 |
| 208 | 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 | 203 | @Test |
| 216 | 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 | 207 | // mock 方法 |
| 223 | 208 | SmsClient mockClient = mock(SmsClient.class); |
| 224 | 209 | when(smsClientFactory.getSmsClient(eq(code))).thenReturn(mockClient); |
| ... | ... | @@ -227,10 +212,6 @@ public class SmsChannelServiceTest extends BaseDbUnitTest { |
| 227 | 212 | SmsClient client = smsChannelService.getSmsClient(code); |
| 228 | 213 | // 断言 |
| 229 | 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 | 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 | 43 | <!-- <dependency>--> |
| ... | ... | @@ -52,11 +52,11 @@ |
| 52 | 52 | <!-- <version>${revision}</version>--> |
| 53 | 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 | 62 | <!-- <dependency>--> |
| ... | ... | @@ -66,26 +66,26 @@ |
| 66 | 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 | 90 | <!-- CRM 相关模块。默认注释,保证编译速度 --> |
| 91 | 91 | <!-- <dependency>--> | ... | ... |
yudao-server/src/main/resources/application-dev.yaml
| ... | ... | @@ -40,12 +40,12 @@ spring: |
| 40 | 40 | primary: master |
| 41 | 41 | datasource: |
| 42 | 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 | 44 | username: root |
| 45 | 45 | password: 123456 |
| 46 | 46 | slave: # 模拟从库,可根据自己需要修改 # 模拟从库,可根据自己需要修改 |
| 47 | 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 | 49 | username: root |
| 50 | 50 | password: 123456 |
| 51 | 51 | ... | ... |
yudao-server/src/main/resources/application-local.yaml
| ... | ... | @@ -45,8 +45,8 @@ spring: |
| 45 | 45 | primary: master |
| 46 | 46 | datasource: |
| 47 | 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 | 50 | # url: jdbc:postgresql://127.0.0.1:5432/ruoyi-vue-pro # PostgreSQL 连接的示例 |
| 51 | 51 | # url: jdbc:oracle:thin:@127.0.0.1:1521:xe # Oracle 连接的示例 |
| 52 | 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 | 63 | # password: Yudao@2024 # OpenGauss 连接的示例 |
| 64 | 64 | slave: # 模拟从库,可根据自己需要修改 |
| 65 | 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 | 67 | username: root |
| 68 | 68 | password: 123456 |
| 69 | 69 | ... | ... |