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

This commit is contained in:
YunaiV 2024-09-07 08:47:56 +08:00
commit 30fd7996f6
71 changed files with 1624 additions and 918 deletions

View File

@ -15,12 +15,12 @@
<!-- 各种 module 拓展 --> <!-- 各种 module 拓展 -->
<module>yudao-module-system</module> <module>yudao-module-system</module>
<module>yudao-module-infra</module> <module>yudao-module-infra</module>
<!-- <module>yudao-module-member</module>--> <module>yudao-module-member</module>
<!-- <module>yudao-module-bpm</module>--> <!-- <module>yudao-module-bpm</module>-->
<!-- <module>yudao-module-report</module>--> <!-- <module>yudao-module-report</module>-->
<!-- <module>yudao-module-mp</module>--> <!-- <module>yudao-module-mp</module>-->
<!-- <module>yudao-module-pay</module>--> <module>yudao-module-pay</module>
<!-- <module>yudao-module-mall</module>--> <module>yudao-module-mall</module>
<!-- <module>yudao-module-crm</module>--> <!-- <module>yudao-module-crm</module>-->
<!-- <module>yudao-module-erp</module>--> <!-- <module>yudao-module-erp</module>-->
<!-- <module>yudao-module-ai</module>--> <!-- <module>yudao-module-ai</module>-->

View File

@ -111,7 +111,7 @@ public class HttpUtils {
authorization = Base64.decodeStr(authorization); authorization = Base64.decodeStr(authorization);
clientId = StrUtil.subBefore(authorization, ":", false); clientId = StrUtil.subBefore(authorization, ":", false);
clientSecret = StrUtil.subAfter(authorization, ":", false); clientSecret = StrUtil.subAfter(authorization, ":", false);
// 再从 Param 中获取 // 再从 Param 中获取
} else { } else {
clientId = request.getParameter("client_id"); clientId = request.getParameter("client_id");
clientSecret = request.getParameter("client_secret"); clientSecret = request.getParameter("client_secret");
@ -143,4 +143,21 @@ public class HttpUtils {
} }
} }
/**
* HTTP get 请求基于 {@link cn.hutool.http.HttpUtil} 实现
*
* 为什么要封装该方法因为 HttpUtil 默认封装的方法没有允许传递 headers 参数
*
* @param url URL
* @param headers 请求头
* @return 请求结果
*/
public static String get(String url, Map<String, String> headers) {
try (HttpResponse response = HttpRequest.get(url)
.addHeaders(headers)
.execute()) {
return response.body();
}
}
} }

View File

@ -91,7 +91,7 @@ public class S3FileClient extends AbstractFileClient<S3FileClientConfig> {
* 开启 VirtualStyle 模式 * 开启 VirtualStyle 模式
*/ */
private void enableVirtualStyleEndpoint() { private void enableVirtualStyleEndpoint() {
if (StrUtil.containsAll(config.getEndpoint(), if (StrUtil.containsAny(config.getEndpoint(),
S3FileClientConfig.ENDPOINT_TENCENT, // 腾讯云 https://cloud.tencent.com/document/product/436/41284 S3FileClientConfig.ENDPOINT_TENCENT, // 腾讯云 https://cloud.tencent.com/document/product/436/41284
S3FileClientConfig.ENDPOINT_VOLCES)) { // 火山云 https://www.volcengine.com/docs/6349/1288493 S3FileClientConfig.ENDPOINT_VOLCES)) { // 火山云 https://www.volcengine.com/docs/6349/1288493
client.enableVirtualStyleEndpoint(); client.enableVirtualStyleEndpoint();

View File

@ -286,6 +286,7 @@ public class ${table.className}ServiceImpl implements ${table.className}Service
// 校验存在 // 校验存在
validate${subSimpleClassName}Exists(${subClassNameVar}.getId()); validate${subSimpleClassName}Exists(${subClassNameVar}.getId());
// 更新 // 更新
${subClassNameVar}.setUpdater(null).setUpdateTime(null); // 解决更新情况下updateTime 不更新
${subClassNameVars.get($index)}Mapper.updateById(${subClassNameVar}); ${subClassNameVars.get($index)}Mapper.updateById(${subClassNameVar});
} }

View File

@ -64,12 +64,11 @@
<el-checkbox <el-checkbox
v-for="dict in $dictMethod(DICT_TYPE.$dictType.toUpperCase())" v-for="dict in $dictMethod(DICT_TYPE.$dictType.toUpperCase())"
:key="dict.value" :key="dict.value"
:label="dict.value" :label="dict.label"
> :value="dict.value"
{{ dict.label }} />
</el-checkbox>
#else##没数据字典 #else##没数据字典
<el-checkbox>请选择字典生成</el-checkbox> <el-checkbox label="请选择字典生成" />
#end #end
</el-checkbox-group> </el-checkbox-group>
</el-form-item> </el-form-item>
@ -85,7 +84,7 @@
{{ dict.label }} {{ dict.label }}
</el-radio> </el-radio>
#else##没数据字典 #else##没数据字典
<el-radio label="1">请选择字典生成</el-radio> <el-radio value="1">请选择字典生成</el-radio>
#end #end
</el-radio-group> </el-radio-group>
</el-form-item> </el-form-item>

View File

@ -92,12 +92,11 @@
<el-checkbox <el-checkbox
v-for="dict in $dictMethod(DICT_TYPE.$dictType.toUpperCase())" v-for="dict in $dictMethod(DICT_TYPE.$dictType.toUpperCase())"
:key="dict.value" :key="dict.value"
:label="dict.value" :label="dict.label"
> :value="dict.value"
{{ dict.label }} />
</el-checkbox>
#else##没数据字典 #else##没数据字典
<el-checkbox>请选择字典生成</el-checkbox> <el-checkbox label="请选择字典生成" />
#end #end
</el-checkbox-group> </el-checkbox-group>
</el-form-item> </el-form-item>
@ -117,7 +116,7 @@
{{ dict.label }} {{ dict.label }}
</el-radio> </el-radio>
#else##没数据字典 #else##没数据字典
<el-radio label="1">请选择字典生成</el-radio> <el-radio value="1">请选择字典生成</el-radio>
#end #end
</el-radio-group> </el-radio-group>
</el-form-item> </el-form-item>
@ -219,12 +218,11 @@
<el-checkbox <el-checkbox
v-for="dict in $dictMethod(DICT_TYPE.$dictType.toUpperCase())" v-for="dict in $dictMethod(DICT_TYPE.$dictType.toUpperCase())"
:key="dict.value" :key="dict.value"
:label="dict.value" :label="dict.label"
> :value="dict.value"
{{ dict.label }} />
</el-checkbox>
#else##没数据字典 #else##没数据字典
<el-checkbox>请选择字典生成</el-checkbox> <el-checkbox label="请选择字典生成" />
#end #end
</el-checkbox-group> </el-checkbox-group>
</el-form-item> </el-form-item>
@ -240,7 +238,7 @@
{{ dict.label }} {{ dict.label }}
</el-radio> </el-radio>
#else##没数据字典 #else##没数据字典
<el-radio label="1">请选择字典生成</el-radio> <el-radio value="1">请选择字典生成</el-radio>
#end #end
</el-radio-group> </el-radio-group>
</el-form-item> </el-form-item>

View File

@ -75,12 +75,11 @@
<el-checkbox <el-checkbox
v-for="dict in $dictMethod(DICT_TYPE.$dictType.toUpperCase())" v-for="dict in $dictMethod(DICT_TYPE.$dictType.toUpperCase())"
:key="dict.value" :key="dict.value"
:label="dict.value" :label="dict.label"
> :value="dict.value"
{{ dict.label }} />
</el-checkbox>
#else##没数据字典 #else##没数据字典
<el-checkbox>请选择字典生成</el-checkbox> <el-checkbox label="请选择字典生成" />
#end #end
</el-checkbox-group> </el-checkbox-group>
</el-form-item> </el-form-item>
@ -96,7 +95,7 @@
{{ dict.label }} {{ dict.label }}
</el-radio> </el-radio>
#else##没数据字典 #else##没数据字典
<el-radio label="1">请选择字典生成</el-radio> <el-radio value="1">请选择字典生成</el-radio>
#end #end
</el-radio-group> </el-radio-group>
</el-form-item> </el-form-item>

View File

@ -3,6 +3,8 @@ package cn.iocoder.yudao.module.product.api.spu.dto;
import cn.iocoder.yudao.module.product.enums.spu.ProductSpuStatusEnum; import cn.iocoder.yudao.module.product.enums.spu.ProductSpuStatusEnum;
import lombok.Data; import lombok.Data;
import java.util.List;
/** /**
* 商品 SPU 信息 Response DTO * 商品 SPU 信息 Response DTO
* *
@ -68,6 +70,13 @@ public class ProductSpuRespDTO {
// ========== 物流相关字段 ========= // ========== 物流相关字段 =========
/**
* 配送方式数组
*
* 对应 DeliveryTypeEnum 枚举
*/
private List<Integer> deliveryTypes;
/** /**
* 物流配置模板编号 * 物流配置模板编号
* *

View File

@ -67,7 +67,7 @@ public class ProductCommentServiceImpl implements ProductCommentService {
// 校验 SPU // 校验 SPU
ProductSpuDO spu = validateSpu(sku.getSpuId()); ProductSpuDO spu = validateSpu(sku.getSpuId());
// 校验评论 // 校验评论
validateCommentExists(createReqDTO.getUserId(), createReqDTO.getOrderId()); validateCommentExists(createReqDTO.getUserId(), createReqDTO.getOrderItemId());
// 获取用户详细信息 // 获取用户详细信息
MemberUserRespDTO user = memberUserApi.getUser(createReqDTO.getUserId()); MemberUserRespDTO user = memberUserApi.getUser(createReqDTO.getUserId());

View File

@ -3,9 +3,11 @@ package cn.iocoder.yudao.module.promotion.api.coupon;
import cn.iocoder.yudao.module.promotion.api.coupon.dto.CouponRespDTO; import cn.iocoder.yudao.module.promotion.api.coupon.dto.CouponRespDTO;
import cn.iocoder.yudao.module.promotion.api.coupon.dto.CouponUseReqDTO; import cn.iocoder.yudao.module.promotion.api.coupon.dto.CouponUseReqDTO;
import cn.iocoder.yudao.module.promotion.api.coupon.dto.CouponValidReqDTO; import cn.iocoder.yudao.module.promotion.api.coupon.dto.CouponValidReqDTO;
import jakarta.validation.Valid; import jakarta.validation.Valid;
import java.util.List;
import java.util.Map;
/** /**
* 优惠劵 API 接口 * 优惠劵 API 接口
* *
@ -35,4 +37,21 @@ public interface CouponApi {
*/ */
CouponRespDTO validateCoupon(@Valid CouponValidReqDTO validReqDTO); CouponRespDTO validateCoupon(@Valid CouponValidReqDTO validReqDTO);
/**
* 管理员给指定用户批量发送优惠券
*
* @param giveCoupons key: 优惠劵模版编号value对应的数量
* @param userId 用户编号
* @return 优惠券编号列表
*/
List<Long> takeCouponsByAdmin(Map<Long, Integer> giveCoupons, Long userId);
/**
* 管理员作废指定用户的指定优惠劵
*
* @param giveCouponIds 赠送的优惠券编号
* @param userId 用户编号
*/
void invalidateCouponsByAdmin(List<Long> giveCouponIds, Long userId);
} }

View File

@ -1,9 +1,14 @@
package cn.iocoder.yudao.module.promotion.api.reward.dto; package cn.iocoder.yudao.module.promotion.api.reward.dto;
import cn.iocoder.yudao.framework.common.enums.CommonStatusEnum;
import cn.iocoder.yudao.module.promotion.enums.common.PromotionConditionTypeEnum; import cn.iocoder.yudao.module.promotion.enums.common.PromotionConditionTypeEnum;
import cn.iocoder.yudao.module.promotion.enums.common.PromotionProductScopeEnum;
import lombok.Data; import lombok.Data;
import java.io.Serializable;
import java.time.LocalDateTime;
import java.util.List; import java.util.List;
import java.util.Map;
/** /**
* 满减送活动的匹配 Response DTO * 满减送活动的匹配 Response DTO
@ -21,28 +26,50 @@ public class RewardActivityMatchRespDTO {
* 活动标题 * 活动标题
*/ */
private String name; private String name;
/**
* 状态
*
* 枚举 {@link CommonStatusEnum}
*/
private Integer status;
/**
* 开始时间
*/
private LocalDateTime startTime;
/**
* 结束时间
*/
private LocalDateTime endTime;
/**
* 备注
*/
private String remark;
/** /**
* 条件类型 * 条件类型
* *
* 枚举 {@link PromotionConditionTypeEnum} * 枚举 {@link PromotionConditionTypeEnum}
*/ */
private Integer conditionType; private Integer conditionType;
/**
* 商品范围
*
* 枚举 {@link PromotionProductScopeEnum}
*/
private Integer productScope;
/**
* 商品 SPU 编号的数组
*/
private List<Long> productScopeValues;
/** /**
* 优惠规则的数组 * 优惠规则的数组
*/ */
private List<Rule> rules; private List<Rule> rules;
/**
* 商品 SPU 编号的数组
*/
private List<Long> spuIds;
// TODO 芋艿后面 RewardActivityRespDTO 有了之后Rule 可以放过去
/** /**
* 优惠规则 * 优惠规则
*/ */
@Data @Data
public static class Rule { public static class Rule implements Serializable {
/** /**
* 优惠门槛 * 优惠门槛
@ -64,13 +91,14 @@ public class RewardActivityMatchRespDTO {
*/ */
private Integer point; private Integer point;
/** /**
* 赠送的优惠劵编号的数组 * 赠送的优惠劵
*
* key: 优惠劵模版编号
* value对应的优惠券数量
*
* 目的用于订单支付后赠送优惠券
*/ */
private List<Long> couponIds; private Map<Long, Integer> giveCouponTemplateCounts;
/**
* 赠送的优惠券数量的数组
*/
private List<Integer> couponCounts;
} }

View File

@ -44,7 +44,8 @@ public interface ErrorCodeConstants {
ErrorCode REWARD_ACTIVITY_UPDATE_FAIL_STATUS_CLOSED = new ErrorCode(1_013_006_002, "满减送活动已关闭,不能修改"); ErrorCode REWARD_ACTIVITY_UPDATE_FAIL_STATUS_CLOSED = new ErrorCode(1_013_006_002, "满减送活动已关闭,不能修改");
ErrorCode REWARD_ACTIVITY_DELETE_FAIL_STATUS_NOT_CLOSED = new ErrorCode(1_013_006_003, "满减送活动未关闭,不能删除"); ErrorCode REWARD_ACTIVITY_DELETE_FAIL_STATUS_NOT_CLOSED = new ErrorCode(1_013_006_003, "满减送活动未关闭,不能删除");
ErrorCode REWARD_ACTIVITY_CLOSE_FAIL_STATUS_CLOSED = new ErrorCode(1_013_006_004, "满减送活动已关闭,不能重复关闭"); ErrorCode REWARD_ACTIVITY_CLOSE_FAIL_STATUS_CLOSED = new ErrorCode(1_013_006_004, "满减送活动已关闭,不能重复关闭");
ErrorCode REWARD_ACTIVITY_CLOSE_FAIL_STATUS_END = new ErrorCode(1_013_006_005, "满减送活动已结束,不能关闭"); ErrorCode REWARD_ACTIVITY_SCOPE_ALL_EXISTS = new ErrorCode(1_013_006_005, "已存在商品范围为全场的满减送活动");
ErrorCode REWARD_ACTIVITY_SCOPE_CATEGORY_EXISTS = new ErrorCode(1_013_006_006, "存在商品类型参加了其它满减送活动");
// ========== TODO 空着 1-013-007-000 ============ // ========== TODO 空着 1-013-007-000 ============

View File

@ -5,6 +5,7 @@ import lombok.AllArgsConstructor;
import lombok.Getter; import lombok.Getter;
import java.util.Arrays; import java.util.Arrays;
import java.util.Objects;
/** /**
* 营销的商品范围枚举 * 营销的商品范围枚举
@ -15,10 +16,9 @@ import java.util.Arrays;
@AllArgsConstructor @AllArgsConstructor
public enum PromotionProductScopeEnum implements IntArrayValuable { public enum PromotionProductScopeEnum implements IntArrayValuable {
ALL(1, "通用券"), // 全部商品 ALL(1, "全部商品"),
SPU(2, "商品券"), // 指定商品 SPU(2, "指定商品"),
CATEGORY(3, "品类券"), // 指定品类 CATEGORY(3, "指定品类");
;
public static final int[] ARRAYS = Arrays.stream(values()).mapToInt(PromotionProductScopeEnum::getScope).toArray(); public static final int[] ARRAYS = Arrays.stream(values()).mapToInt(PromotionProductScopeEnum::getScope).toArray();
@ -36,4 +36,16 @@ public enum PromotionProductScopeEnum implements IntArrayValuable {
return ARRAYS; return ARRAYS;
} }
public static boolean isAll(Integer scope) {
return Objects.equals(scope, ALL.scope);
}
public static boolean isSpu(Integer scope) {
return Objects.equals(scope, SPU.scope);
}
public static boolean isCategory(Integer scope) {
return Objects.equals(scope, CATEGORY.scope);
}
} }

View File

@ -17,8 +17,7 @@ public enum CouponStatusEnum implements IntArrayValuable {
UNUSED(1, "未使用"), UNUSED(1, "未使用"),
USED(2, "已使用"), USED(2, "已使用"),
EXPIRE(3, "已过期"), EXPIRE(3, "已过期");
;
public static final int[] ARRAYS = Arrays.stream(values()).mapToInt(CouponStatusEnum::getStatus).toArray(); public static final int[] ARRAYS = Arrays.stream(values()).mapToInt(CouponStatusEnum::getStatus).toArray();

View File

@ -7,10 +7,12 @@ import cn.iocoder.yudao.module.promotion.api.coupon.dto.CouponValidReqDTO;
import cn.iocoder.yudao.module.promotion.convert.coupon.CouponConvert; import cn.iocoder.yudao.module.promotion.convert.coupon.CouponConvert;
import cn.iocoder.yudao.module.promotion.dal.dataobject.coupon.CouponDO; import cn.iocoder.yudao.module.promotion.dal.dataobject.coupon.CouponDO;
import cn.iocoder.yudao.module.promotion.service.coupon.CouponService; import cn.iocoder.yudao.module.promotion.service.coupon.CouponService;
import jakarta.annotation.Resource;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
import org.springframework.validation.annotation.Validated; import org.springframework.validation.annotation.Validated;
import jakarta.annotation.Resource; import java.util.List;
import java.util.Map;
/** /**
* 优惠劵 API 实现类 * 优惠劵 API 实现类
@ -41,4 +43,14 @@ public class CouponApiImpl implements CouponApi {
return CouponConvert.INSTANCE.convert(coupon); return CouponConvert.INSTANCE.convert(coupon);
} }
@Override
public List<Long> takeCouponsByAdmin(Map<Long, Integer> giveCoupons, Long userId) {
return couponService.takeCouponsByAdmin(giveCoupons, userId);
}
@Override
public void invalidateCouponsByAdmin(List<Long> giveCouponIds, Long userId) {
couponService.invalidateCouponsByAdmin(giveCouponIds, userId);
}
} }

View File

@ -2,23 +2,22 @@ package cn.iocoder.yudao.module.promotion.controller.admin.reward;
import cn.iocoder.yudao.framework.common.pojo.CommonResult; import cn.iocoder.yudao.framework.common.pojo.CommonResult;
import cn.iocoder.yudao.framework.common.pojo.PageResult; import cn.iocoder.yudao.framework.common.pojo.PageResult;
import cn.iocoder.yudao.framework.common.util.object.BeanUtils;
import cn.iocoder.yudao.module.promotion.controller.admin.reward.vo.RewardActivityCreateReqVO; import cn.iocoder.yudao.module.promotion.controller.admin.reward.vo.RewardActivityCreateReqVO;
import cn.iocoder.yudao.module.promotion.controller.admin.reward.vo.RewardActivityPageReqVO; import cn.iocoder.yudao.module.promotion.controller.admin.reward.vo.RewardActivityPageReqVO;
import cn.iocoder.yudao.module.promotion.controller.admin.reward.vo.RewardActivityRespVO; import cn.iocoder.yudao.module.promotion.controller.admin.reward.vo.RewardActivityRespVO;
import cn.iocoder.yudao.module.promotion.controller.admin.reward.vo.RewardActivityUpdateReqVO; import cn.iocoder.yudao.module.promotion.controller.admin.reward.vo.RewardActivityUpdateReqVO;
import cn.iocoder.yudao.module.promotion.convert.reward.RewardActivityConvert;
import cn.iocoder.yudao.module.promotion.dal.dataobject.reward.RewardActivityDO; import cn.iocoder.yudao.module.promotion.dal.dataobject.reward.RewardActivityDO;
import cn.iocoder.yudao.module.promotion.service.reward.RewardActivityService; import cn.iocoder.yudao.module.promotion.service.reward.RewardActivityService;
import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter; import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.tags.Tag; import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.annotation.Resource;
import jakarta.validation.Valid;
import org.springframework.security.access.prepost.PreAuthorize; import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.validation.annotation.Validated; import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.*; import org.springframework.web.bind.annotation.*;
import jakarta.annotation.Resource;
import jakarta.validation.Valid;
import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success; import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success;
@Tag(name = "管理后台 - 满减送活动") @Tag(name = "管理后台 - 满减送活动")
@ -69,7 +68,7 @@ public class RewardActivityController {
@PreAuthorize("@ss.hasPermission('promotion:reward-activity:query')") @PreAuthorize("@ss.hasPermission('promotion:reward-activity:query')")
public CommonResult<RewardActivityRespVO> getRewardActivity(@RequestParam("id") Long id) { public CommonResult<RewardActivityRespVO> getRewardActivity(@RequestParam("id") Long id) {
RewardActivityDO rewardActivity = rewardActivityService.getRewardActivity(id); RewardActivityDO rewardActivity = rewardActivityService.getRewardActivity(id);
return success(RewardActivityConvert.INSTANCE.convert(rewardActivity)); return success(BeanUtils.toBean(rewardActivity, RewardActivityRespVO.class));
} }
@GetMapping("/page") @GetMapping("/page")
@ -77,7 +76,7 @@ public class RewardActivityController {
@PreAuthorize("@ss.hasPermission('promotion:reward-activity:query')") @PreAuthorize("@ss.hasPermission('promotion:reward-activity:query')")
public CommonResult<PageResult<RewardActivityRespVO>> getRewardActivityPage(@Valid RewardActivityPageReqVO pageVO) { public CommonResult<PageResult<RewardActivityRespVO>> getRewardActivityPage(@Valid RewardActivityPageReqVO pageVO) {
PageResult<RewardActivityDO> pageResult = rewardActivityService.getRewardActivityPage(pageVO); PageResult<RewardActivityDO> pageResult = rewardActivityService.getRewardActivityPage(pageVO);
return success(RewardActivityConvert.INSTANCE.convertPage(pageResult)); return success(BeanUtils.toBean(pageResult, RewardActivityRespVO.class));
} }
} }

View File

@ -6,18 +6,17 @@ import cn.iocoder.yudao.module.promotion.enums.common.PromotionConditionTypeEnum
import cn.iocoder.yudao.module.promotion.enums.common.PromotionProductScopeEnum; import cn.iocoder.yudao.module.promotion.enums.common.PromotionProductScopeEnum;
import com.fasterxml.jackson.annotation.JsonIgnore; import com.fasterxml.jackson.annotation.JsonIgnore;
import io.swagger.v3.oas.annotations.media.Schema; import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import org.springframework.format.annotation.DateTimeFormat;
import jakarta.validation.Valid; import jakarta.validation.Valid;
import jakarta.validation.constraints.AssertTrue; import jakarta.validation.constraints.AssertTrue;
import jakarta.validation.constraints.Future; import jakarta.validation.constraints.Future;
import jakarta.validation.constraints.Min; import jakarta.validation.constraints.Min;
import jakarta.validation.constraints.NotNull; import jakarta.validation.constraints.NotNull;
import lombok.Data;
import java.time.LocalDateTime; import java.time.LocalDateTime;
import java.util.List; import java.util.List;
import java.util.Map;
import static cn.iocoder.yudao.framework.common.util.date.DateUtils.FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND; import java.util.Objects;
/** /**
* 满减送活动 Base VO提供给添加修改详细的子 VO 使用 * 满减送活动 Base VO提供给添加修改详细的子 VO 使用
@ -32,12 +31,10 @@ public class RewardActivityBaseVO {
@Schema(description = "开始时间", requiredMode = Schema.RequiredMode.REQUIRED) @Schema(description = "开始时间", requiredMode = Schema.RequiredMode.REQUIRED)
@NotNull(message = "开始时间不能为空") @NotNull(message = "开始时间不能为空")
@DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND)
private LocalDateTime startTime; private LocalDateTime startTime;
@Schema(description = "结束时间", requiredMode = Schema.RequiredMode.REQUIRED) @Schema(description = "结束时间", requiredMode = Schema.RequiredMode.REQUIRED)
@NotNull(message = "结束时间不能为空") @NotNull(message = "结束时间不能为空")
@DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND)
@Future(message = "结束时间必须大于当前时间") @Future(message = "结束时间必须大于当前时间")
private LocalDateTime endTime; private LocalDateTime endTime;
@ -54,8 +51,8 @@ public class RewardActivityBaseVO {
@InEnum(value = PromotionProductScopeEnum.class, message = "商品范围必须是 {value}") @InEnum(value = PromotionProductScopeEnum.class, message = "商品范围必须是 {value}")
private Integer productScope; private Integer productScope;
@Schema(description = "商品 SPU 编号的数组", example = "1,2,3") @Schema(description = "商品范围编号的数组", example = "[1, 3]")
private List<Long> productSpuIds; private List<Long> productScopeValues;
/** /**
* 优惠规则的数组 * 优惠规则的数组
@ -76,24 +73,28 @@ public class RewardActivityBaseVO {
private Integer discountPrice; private Integer discountPrice;
@Schema(description = "是否包邮", requiredMode = Schema.RequiredMode.REQUIRED, example = "true") @Schema(description = "是否包邮", requiredMode = Schema.RequiredMode.REQUIRED, example = "true")
@NotNull(message = "规则是否包邮不能为空")
private Boolean freeDelivery; private Boolean freeDelivery;
@Schema(description = "赠送的积分", requiredMode = Schema.RequiredMode.REQUIRED, example = "100") @Schema(description = "赠送的积分", requiredMode = Schema.RequiredMode.REQUIRED, example = "100")
@Min(value = 1L, message = "赠送的积分必须大于等于 1")
private Integer point; private Integer point;
@Schema(description = "赠送的优惠劵编号的数组", example = "1,2,3") @Schema(description = "赠送的优惠劵编号的数组")
private List<Long> couponIds; private Map<Long, Integer> giveCouponTemplateCounts;
@Schema(description = "赠送的优惠券数量的数组", example = "1,2,3") @AssertTrue(message = "赠送的积分不能小于 0")
private List<Integer> couponCounts;
@AssertTrue(message = "优惠劵和数量必须一一对应")
@JsonIgnore @JsonIgnore
public boolean isCouponCountsValid() { public boolean isPointValid() {
return CollUtil.size(couponCounts) == CollUtil.size(couponCounts); return point == null || point >= 0;
} }
} }
@AssertTrue(message = "商品范围编号的数组不能为空")
@JsonIgnore
public boolean isProductScopeValuesValid() {
return Objects.equals(productScope, PromotionProductScopeEnum.ALL.getScope()) // 全部范围时可以为空
|| CollUtil.isNotEmpty(productScopeValues);
}
} }

View File

@ -2,8 +2,11 @@ package cn.iocoder.yudao.module.promotion.controller.app.activity;
import cn.hutool.core.collection.CollUtil; import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.map.MapUtil; import cn.hutool.core.map.MapUtil;
import cn.hutool.core.util.ObjUtil;
import cn.iocoder.yudao.framework.common.enums.CommonStatusEnum; import cn.iocoder.yudao.framework.common.enums.CommonStatusEnum;
import cn.iocoder.yudao.framework.common.pojo.CommonResult; import cn.iocoder.yudao.framework.common.pojo.CommonResult;
import cn.iocoder.yudao.module.product.api.spu.ProductSpuApi;
import cn.iocoder.yudao.module.product.api.spu.dto.ProductSpuRespDTO;
import cn.iocoder.yudao.module.promotion.controller.app.activity.vo.AppActivityRespVO; import cn.iocoder.yudao.module.promotion.controller.app.activity.vo.AppActivityRespVO;
import cn.iocoder.yudao.module.promotion.dal.dataobject.bargain.BargainActivityDO; import cn.iocoder.yudao.module.promotion.dal.dataobject.bargain.BargainActivityDO;
import cn.iocoder.yudao.module.promotion.dal.dataobject.combination.CombinationActivityDO; import cn.iocoder.yudao.module.promotion.dal.dataobject.combination.CombinationActivityDO;
@ -11,7 +14,7 @@ import cn.iocoder.yudao.module.promotion.dal.dataobject.discount.DiscountActivit
import cn.iocoder.yudao.module.promotion.dal.dataobject.discount.DiscountProductDO; import cn.iocoder.yudao.module.promotion.dal.dataobject.discount.DiscountProductDO;
import cn.iocoder.yudao.module.promotion.dal.dataobject.reward.RewardActivityDO; import cn.iocoder.yudao.module.promotion.dal.dataobject.reward.RewardActivityDO;
import cn.iocoder.yudao.module.promotion.dal.dataobject.seckill.SeckillActivityDO; import cn.iocoder.yudao.module.promotion.dal.dataobject.seckill.SeckillActivityDO;
import cn.iocoder.yudao.module.promotion.enums.common.PromotionActivityStatusEnum; import cn.iocoder.yudao.module.promotion.enums.common.PromotionProductScopeEnum;
import cn.iocoder.yudao.module.promotion.enums.common.PromotionTypeEnum; import cn.iocoder.yudao.module.promotion.enums.common.PromotionTypeEnum;
import cn.iocoder.yudao.module.promotion.service.bargain.BargainActivityService; import cn.iocoder.yudao.module.promotion.service.bargain.BargainActivityService;
import cn.iocoder.yudao.module.promotion.service.combination.CombinationActivityService; import cn.iocoder.yudao.module.promotion.service.combination.CombinationActivityService;
@ -30,7 +33,6 @@ import org.springframework.web.bind.annotation.RestController;
import java.time.LocalDateTime; import java.time.LocalDateTime;
import java.util.*; import java.util.*;
import java.util.stream.Collectors;
import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success; import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success;
import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.*; import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.*;
@ -52,6 +54,9 @@ public class AppActivityController {
@Resource @Resource
private RewardActivityService rewardActivityService; private RewardActivityService rewardActivityService;
@Resource
private ProductSpuApi productSpuApi;
@GetMapping("/list-by-spu-id") @GetMapping("/list-by-spu-id")
@Operation(summary = "获得单个商品,近期参与的每个活动") @Operation(summary = "获得单个商品,近期参与的每个活动")
@Parameter(name = "spuId", description = "商品编号", required = true) @Parameter(name = "spuId", description = "商品编号", required = true)
@ -87,7 +92,7 @@ public class AppActivityController {
// 4. 限时折扣活动 // 4. 限时折扣活动
getDiscountActivities(spuIds, now, activityList); getDiscountActivities(spuIds, now, activityList);
// 5. 满减送活动 // 5. 满减送活动
getRewardActivities(spuIds, now, activityList); getRewardActivityList(spuIds, now, activityList);
return activityList; return activityList;
} }
@ -144,28 +149,51 @@ public class AppActivityController {
item.getName(), productMap.get(item.getId()), item.getStartTime(), item.getEndTime()))); item.getName(), productMap.get(item.getId()), item.getStartTime(), item.getEndTime())));
} }
private void getRewardActivities(Collection<Long> spuIds, LocalDateTime now, List<AppActivityRespVO> activityList) { private void getRewardActivityList(Collection<Long> spuIds, LocalDateTime now, List<AppActivityRespVO> activityList) {
// TODO @puhui999 3 范围不只 spuId还有 categoryId全部 // 1.1 获得所有的活动
List<RewardActivityDO> rewardActivityList = rewardActivityService.getRewardActivityBySpuIdsAndStatusAndDateTimeLt( List<RewardActivityDO> rewardActivityList = rewardActivityService.getRewardActivityListByStatusAndDateTimeLt(
spuIds, PromotionActivityStatusEnum.RUN.getStatus(), now); CommonStatusEnum.ENABLE.getStatus(), now);
if (CollUtil.isEmpty(rewardActivityList)) { if (CollUtil.isEmpty(rewardActivityList)) {
return; return;
} }
// 1.2 获得所有的商品信息
List<ProductSpuRespDTO> spuList = productSpuApi.getSpuList(spuIds);
if (CollUtil.isEmpty(spuList)) {
return;
}
Map<Long, Optional<RewardActivityDO>> spuIdAndActivityMap = spuIds.stream() // 2. 构建活动
.collect(Collectors.toMap( for (RewardActivityDO rewardActivity : rewardActivityList) {
spuId -> spuId, // 情况一所有商品都能参加
spuId -> rewardActivityList.stream() if (PromotionProductScopeEnum.isAll(rewardActivity.getProductScope())) {
.filter(activity -> activity.getProductSpuIds().contains(spuId)) buildAppActivityRespVO(rewardActivity, spuIds, activityList);
.max(Comparator.comparing(RewardActivityDO::getCreateTime)))); }
for (Long supId : spuIdAndActivityMap.keySet()) { // 情况二指定商品参加
if (spuIdAndActivityMap.get(supId).isEmpty()) { if (PromotionProductScopeEnum.isSpu(rewardActivity.getProductScope())) {
List<Long> fSpuIds = spuList.stream().map(ProductSpuRespDTO::getId).filter(id ->
rewardActivity.getProductScopeValues().contains(id)).toList();
buildAppActivityRespVO(rewardActivity, fSpuIds, activityList);
}
// 情况三指定商品类型参加
if (PromotionProductScopeEnum.isCategory(rewardActivity.getProductScope())) {
List<Long> fSpuIds = spuList.stream().filter(spuItem -> rewardActivity.getProductScopeValues()
.contains(spuItem.getCategoryId())).map(ProductSpuRespDTO::getId).toList();
buildAppActivityRespVO(rewardActivity, fSpuIds, activityList);
}
}
}
private static void buildAppActivityRespVO(RewardActivityDO rewardActivity, Collection<Long> spuIds,
List<AppActivityRespVO> activityList) {
for (Long spuId : spuIds) {
// 校验商品是否已经加入过活动
if (anyMatch(activityList, appActivity -> ObjUtil.equal(appActivity.getId(), rewardActivity.getId()) &&
ObjUtil.equal(appActivity.getSpuId(), spuId))) {
continue; continue;
} }
activityList.add(new AppActivityRespVO(rewardActivity.getId(),
RewardActivityDO rewardActivityDO = spuIdAndActivityMap.get(supId).get(); PromotionTypeEnum.REWARD_ACTIVITY.getType(), rewardActivity.getName(), spuId,
activityList.add(new AppActivityRespVO(rewardActivityDO.getId(), PromotionTypeEnum.REWARD_ACTIVITY.getType(), rewardActivity.getStartTime(), rewardActivity.getEndTime()));
rewardActivityDO.getName(), supId, rewardActivityDO.getStartTime(), rewardActivityDO.getEndTime()));
} }
} }

View File

@ -1,29 +0,0 @@
package cn.iocoder.yudao.module.promotion.convert.reward;
import cn.iocoder.yudao.framework.common.pojo.PageResult;
import cn.iocoder.yudao.module.promotion.controller.admin.reward.vo.RewardActivityCreateReqVO;
import cn.iocoder.yudao.module.promotion.controller.admin.reward.vo.RewardActivityRespVO;
import cn.iocoder.yudao.module.promotion.controller.admin.reward.vo.RewardActivityUpdateReqVO;
import cn.iocoder.yudao.module.promotion.dal.dataobject.reward.RewardActivityDO;
import org.mapstruct.Mapper;
import org.mapstruct.factory.Mappers;
/**
* 满减送活动 Convert
*
* @author 芋道源码
*/
@Mapper
public interface RewardActivityConvert {
RewardActivityConvert INSTANCE = Mappers.getMapper(RewardActivityConvert.class);
RewardActivityDO convert(RewardActivityCreateReqVO bean);
RewardActivityDO convert(RewardActivityUpdateReqVO bean);
RewardActivityRespVO convert(RewardActivityDO bean);
PageResult<RewardActivityRespVO> convertPage(PageResult<RewardActivityDO> page);
}

View File

@ -50,7 +50,6 @@ public class CouponDO extends BaseDO {
* *
* 枚举 {@link CouponStatusEnum} * 枚举 {@link CouponStatusEnum}
*/ */
// TODO 芋艿已作废
private Integer status; private Integer status;
// TODO 芋艿发放 adminid // TODO 芋艿发放 adminid

View File

@ -1,8 +1,8 @@
package cn.iocoder.yudao.module.promotion.dal.dataobject.reward; package cn.iocoder.yudao.module.promotion.dal.dataobject.reward;
import cn.iocoder.yudao.framework.common.enums.CommonStatusEnum;
import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO; import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO;
import cn.iocoder.yudao.framework.mybatis.core.type.LongListTypeHandler; import cn.iocoder.yudao.framework.mybatis.core.type.LongListTypeHandler;
import cn.iocoder.yudao.module.promotion.enums.common.PromotionActivityStatusEnum;
import cn.iocoder.yudao.module.promotion.enums.common.PromotionConditionTypeEnum; import cn.iocoder.yudao.module.promotion.enums.common.PromotionConditionTypeEnum;
import cn.iocoder.yudao.module.promotion.enums.common.PromotionProductScopeEnum; import cn.iocoder.yudao.module.promotion.enums.common.PromotionProductScopeEnum;
import com.baomidou.mybatisplus.annotation.KeySequence; import com.baomidou.mybatisplus.annotation.KeySequence;
@ -16,6 +16,7 @@ import lombok.EqualsAndHashCode;
import java.io.Serializable; import java.io.Serializable;
import java.time.LocalDateTime; import java.time.LocalDateTime;
import java.util.List; import java.util.List;
import java.util.Map;
/** /**
* 满减送活动 DO * 满减送活动 DO
@ -40,7 +41,7 @@ public class RewardActivityDO extends BaseDO {
/** /**
* 状态 * 状态
* *
* 枚举 {@link PromotionActivityStatusEnum} * 枚举 {@link CommonStatusEnum}
*/ */
private Integer status; private Integer status;
/** /**
@ -71,7 +72,7 @@ public class RewardActivityDO extends BaseDO {
* 商品 SPU 编号的数组 * 商品 SPU 编号的数组
*/ */
@TableField(typeHandler = LongListTypeHandler.class) @TableField(typeHandler = LongListTypeHandler.class)
private List<Long> productSpuIds; private List<Long> productScopeValues;
/** /**
* 优惠规则的数组 * 优惠规则的数组
*/ */
@ -104,13 +105,14 @@ public class RewardActivityDO extends BaseDO {
*/ */
private Integer point; private Integer point;
/** /**
* 赠送的优惠劵编号的数组 * 赠送的优惠劵
*
* key: 优惠劵模版编号
* value对应的优惠券数量
*
* 目的用于订单支付后赠送优惠券
*/ */
private List<Long> couponIds; private Map<Long, Integer> giveCouponTemplateCounts;
/**
* 赠送的优惠券数量的数组
*/
private List<Integer> couponCounts;
} }

View File

@ -30,10 +30,6 @@ public interface RewardActivityMapper extends BaseMapperX<RewardActivityDO> {
.orderByDesc(RewardActivityDO::getId)); .orderByDesc(RewardActivityDO::getId));
} }
default List<RewardActivityDO> selectListByStatus(Collection<Integer> statuses) {
return selectList(RewardActivityDO::getStatus, statuses);
}
default List<RewardActivityDO> selectListByProductScopeAndStatus(Integer productScope, Integer status) { default List<RewardActivityDO> selectListByProductScopeAndStatus(Integer productScope, Integer status) {
return selectList(new LambdaQueryWrapperX<RewardActivityDO>() return selectList(new LambdaQueryWrapperX<RewardActivityDO>()
.eq(RewardActivityDO::getProductScope, productScope) .eq(RewardActivityDO::getProductScope, productScope)
@ -53,16 +49,16 @@ public interface RewardActivityMapper extends BaseMapperX<RewardActivityDO> {
* 获取指定活动编号的活动列表且 * 获取指定活动编号的活动列表且
* 开始时间和结束时间小于给定时间 dateTime 的活动列表 * 开始时间和结束时间小于给定时间 dateTime 的活动列表
* *
* @param ids 活动编号 * @param status 状态
* @param dateTime 指定日期 * @param dateTime 指定日期
* @return 活动列表 * @return 活动列表
*/ */
default List<RewardActivityDO> selectListByIdsAndDateTimeLt(Collection<Long> ids, LocalDateTime dateTime) { default List<RewardActivityDO> selectListByStatusAndDateTimeLt(Integer status, LocalDateTime dateTime) {
return selectList(new LambdaQueryWrapperX<RewardActivityDO>() return selectList(new LambdaQueryWrapperX<RewardActivityDO>()
.in(RewardActivityDO::getId, ids) .eq(RewardActivityDO::getStatus, status)
.lt(RewardActivityDO::getStartTime, dateTime) .lt(RewardActivityDO::getStartTime, dateTime)
.gt(RewardActivityDO::getEndTime, dateTime)// 开始时间 < 指定时间 < 结束时间也就是说获取指定时间段的活动 .gt(RewardActivityDO::getEndTime, dateTime)// 开始时间 < 指定时间 < 结束时间也就是说获取指定时间段的活动
.orderByDesc(RewardActivityDO::getCreateTime) .orderByAsc(RewardActivityDO::getStartTime)
); );
} }

View File

@ -27,6 +27,7 @@ import cn.iocoder.yudao.module.promotion.enums.combination.CombinationRecordStat
import cn.iocoder.yudao.module.system.api.social.SocialClientApi; import cn.iocoder.yudao.module.system.api.social.SocialClientApi;
import cn.iocoder.yudao.module.system.api.social.dto.SocialWxaSubscribeMessageSendReqDTO; import cn.iocoder.yudao.module.system.api.social.dto.SocialWxaSubscribeMessageSendReqDTO;
import cn.iocoder.yudao.module.trade.api.order.TradeOrderApi; import cn.iocoder.yudao.module.trade.api.order.TradeOrderApi;
import cn.iocoder.yudao.module.trade.enums.order.TradeOrderCancelTypeEnum;
import jakarta.annotation.Nullable; import jakarta.annotation.Nullable;
import jakarta.annotation.Resource; import jakarta.annotation.Resource;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
@ -37,7 +38,10 @@ import org.springframework.transaction.annotation.Transactional;
import org.springframework.validation.annotation.Validated; import org.springframework.validation.annotation.Validated;
import java.time.LocalDateTime; import java.time.LocalDateTime;
import java.util.*; import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import java.util.Map;
import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception; import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception;
import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.*; import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.*;
@ -335,7 +339,8 @@ public class CombinationRecordServiceImpl implements CombinationRecordService {
List<CombinationRecordDO> headAndRecords = updateBatchCombinationRecords(headRecord, List<CombinationRecordDO> headAndRecords = updateBatchCombinationRecords(headRecord,
CombinationRecordStatusEnum.FAILED); CombinationRecordStatusEnum.FAILED);
// 2. 订单取消 // 2. 订单取消
headAndRecords.forEach(item -> tradeOrderApi.cancelPaidOrder(item.getUserId(), item.getOrderId())); headAndRecords.forEach(item -> tradeOrderApi.cancelPaidOrder(item.getUserId(), item.getOrderId(),
TradeOrderCancelTypeEnum.COMBINATION_CLOSE.getType()));
} }
/** /**

View File

@ -38,14 +38,6 @@ public interface CouponService {
*/ */
void validCoupon(CouponDO coupon); void validCoupon(CouponDO coupon);
/**
* 获得优惠劵分页
*
* @param pageReqVO 分页查询
* @return 优惠劵分页
*/
PageResult<CouponDO> getCouponPage(CouponPageReqVO pageReqVO);
/** /**
* 使用优惠劵 * 使用优惠劵
* *
@ -69,42 +61,44 @@ public interface CouponService {
*/ */
void deleteCoupon(Long id); void deleteCoupon(Long id);
/**
* 获得用户的优惠劵列表
*
* @param userId 用户编号
* @param status 优惠劵状态
* @return 优惠劵列表
*/
List<CouponDO> getCouponList(Long userId, Integer status);
/**
* 获得未使用的优惠劵数量
*
* @param userId 用户编号
* @return 未使用的优惠劵数量
*/
Long getUnusedCouponCount(Long userId);
/** /**
* 领取优惠券 * 领取优惠券
* *
* @param templateId 优惠券模板编号 * @param templateId 优惠券模板编号
* @param userIds 用户编号列表 * @param userIds 用户编号列表
* @param takeType 领取方式 * @param takeType 领取方式
* @return key: userId, value: 优惠券编号列表
*/ */
void takeCoupon(Long templateId, Set<Long> userIds, CouponTakeTypeEnum takeType); Map<Long, List<Long>> takeCoupon(Long templateId, Set<Long> userIds, CouponTakeTypeEnum takeType);
/** /**
* 管理员给用户发送优惠券 * 管理员给用户发送优惠券
* *
* @param templateId 优惠券模板编号 * @param templateId 优惠券模板编号
* @param userIds 用户编号列表 * @param userIds 用户编号列表
* @return key: userId, value: 优惠券编号列表
*/ */
default void takeCouponByAdmin(Long templateId, Set<Long> userIds) { default Map<Long, List<Long>> takeCouponByAdmin(Long templateId, Set<Long> userIds) {
takeCoupon(templateId, userIds, CouponTakeTypeEnum.ADMIN); return takeCoupon(templateId, userIds, CouponTakeTypeEnum.ADMIN);
} }
/**
* 管理员给指定用户批量发送优惠券
*
* @param giveCoupons key: 优惠劵模版编号value对应的数量
* @param userId 用户编号
* @return 优惠券编号列表
*/
List<Long> takeCouponsByAdmin(Map<Long, Integer> giveCoupons, Long userId);
/**
* 管理员作废指定用户的指定优惠劵
*
* @param giveCouponIds 赠送的优惠券编号
* @param userId 用户编号
*/
void invalidateCouponsByAdmin(List<Long> giveCouponIds, Long userId);
/** /**
* 会员领取优惠券 * 会员领取优惠券
* *
@ -122,6 +116,49 @@ public interface CouponService {
*/ */
void takeCouponByRegister(Long userId); void takeCouponByRegister(Long userId);
/**
* 过期优惠券
*
* @return 过期数量
*/
int expireCoupon();
// ======================= 查询相关 =======================
/**
* 获得未使用的优惠劵数量
*
* @param userId 用户编号
* @return 未使用的优惠劵数量
*/
Long getUnusedCouponCount(Long userId);
/**
* 获得优惠劵分页
*
* @param pageReqVO 分页查询
* @return 优惠劵分页
*/
PageResult<CouponDO> getCouponPage(CouponPageReqVO pageReqVO);
/**
* 获得用户的优惠劵列表
*
* @param userId 用户编号
* @param status 优惠劵状态
* @return 优惠劵列表
*/
List<CouponDO> getCouponList(Long userId, Integer status);
/**
* 统计会员领取优惠券的数量
*
* @param templateIds 优惠券模板编号列表
* @param userId 用户编号
* @return 领取优惠券的数量
*/
Map<Long, Integer> getTakeCountMapByTemplateIds(Collection<Long> templateIds, Long userId);
/** /**
* 获取会员领取指定优惠券的数量 * 获取会员领取指定优惠券的数量
* *
@ -134,15 +171,6 @@ public interface CouponService {
return MapUtil.getInt(map, templateId, 0); return MapUtil.getInt(map, templateId, 0);
} }
/**
* 统计会员领取优惠券的数量
*
* @param templateIds 优惠券模板编号列表
* @param userId 用户编号
* @return 领取优惠券的数量
*/
Map<Long, Integer> getTakeCountMapByTemplateIds(Collection<Long> templateIds, Long userId);
/** /**
* 获取用户匹配的优惠券列表 * 获取用户匹配的优惠券列表
* *
@ -152,13 +180,6 @@ public interface CouponService {
*/ */
List<CouponDO> getMatchCouponList(Long userId, AppCouponMatchReqVO matchReqVO); List<CouponDO> getMatchCouponList(Long userId, AppCouponMatchReqVO matchReqVO);
/**
* 过期优惠券
*
* @return 过期数量
*/
int expireCoupon();
/** /**
* 获取用户是否可以领取优惠券 * 获取用户是否可以领取优惠券
* *

View File

@ -3,6 +3,7 @@ package cn.iocoder.yudao.module.promotion.service.coupon;
import cn.hutool.core.collection.CollStreamUtil; import cn.hutool.core.collection.CollStreamUtil;
import cn.hutool.core.collection.CollUtil; import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.map.MapUtil; import cn.hutool.core.map.MapUtil;
import cn.hutool.core.util.ObjUtil;
import cn.hutool.core.util.ObjectUtil; import cn.hutool.core.util.ObjectUtil;
import cn.hutool.core.util.StrUtil; import cn.hutool.core.util.StrUtil;
import cn.hutool.extra.spring.SpringUtil; import cn.hutool.extra.spring.SpringUtil;
@ -19,19 +20,19 @@ import cn.iocoder.yudao.module.promotion.dal.mysql.coupon.CouponMapper;
import cn.iocoder.yudao.module.promotion.enums.coupon.CouponStatusEnum; import cn.iocoder.yudao.module.promotion.enums.coupon.CouponStatusEnum;
import cn.iocoder.yudao.module.promotion.enums.coupon.CouponTakeTypeEnum; import cn.iocoder.yudao.module.promotion.enums.coupon.CouponTakeTypeEnum;
import cn.iocoder.yudao.module.promotion.enums.coupon.CouponTemplateValidityTypeEnum; import cn.iocoder.yudao.module.promotion.enums.coupon.CouponTemplateValidityTypeEnum;
import jakarta.annotation.Resource;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional; import org.springframework.transaction.annotation.Transactional;
import org.springframework.validation.annotation.Validated; import org.springframework.validation.annotation.Validated;
import jakarta.annotation.Resource;
import java.time.LocalDateTime; import java.time.LocalDateTime;
import java.util.*; import java.util.*;
import java.util.stream.Collectors; import java.util.stream.Collectors;
import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception; import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception;
import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.*; import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.*;
import static cn.iocoder.yudao.framework.common.util.collection.MapUtils.findAndThen;
import static cn.iocoder.yudao.module.promotion.enums.ErrorCodeConstants.*; import static cn.iocoder.yudao.module.promotion.enums.ErrorCodeConstants.*;
import static java.util.Arrays.asList; import static java.util.Arrays.asList;
@ -76,20 +77,6 @@ public class CouponServiceImpl implements CouponService {
} }
} }
@Override
public PageResult<CouponDO> getCouponPage(CouponPageReqVO pageReqVO) {
// 获得用户编号
if (StrUtil.isNotEmpty(pageReqVO.getNickname())) {
List<MemberUserRespDTO> users = memberUserApi.getUserListByNickname(pageReqVO.getNickname());
if (CollUtil.isEmpty(users)) {
return PageResult.empty();
}
pageReqVO.setUserIds(convertSet(users, MemberUserRespDTO::getId));
}
// 分页查询
return couponMapper.selectPage(pageReqVO);
}
@Override @Override
public void useCoupon(Long id, Long userId, Long orderId) { public void useCoupon(Long id, Long userId, Long orderId) {
// 校验优惠劵 // 校验优惠劵
@ -147,25 +134,8 @@ public class CouponServiceImpl implements CouponService {
} }
@Override @Override
public List<CouponDO> getCouponList(Long userId, Integer status) { @Transactional(rollbackFor = Exception.class)
return couponMapper.selectListByUserIdAndStatus(userId, status); public Map<Long, List<Long>> takeCoupon(Long templateId, Set<Long> userIds, CouponTakeTypeEnum takeType) {
}
private CouponDO validateCouponExists(Long id) {
CouponDO coupon = couponMapper.selectById(id);
if (coupon == null) {
throw exception(COUPON_NOT_EXISTS);
}
return coupon;
}
@Override
public Long getUnusedCouponCount(Long userId) {
return couponMapper.selectCountByUserIdAndStatus(userId, CouponStatusEnum.UNUSED.getStatus());
}
@Override
public void takeCoupon(Long templateId, Set<Long> userIds, CouponTakeTypeEnum takeType) {
CouponTemplateDO template = couponTemplateService.getCouponTemplate(templateId); CouponTemplateDO template = couponTemplateService.getCouponTemplate(templateId);
// 1. 过滤掉达到领取限制的用户 // 1. 过滤掉达到领取限制的用户
removeTakeLimitUser(userIds, template); removeTakeLimitUser(userIds, template);
@ -173,10 +143,77 @@ public class CouponServiceImpl implements CouponService {
validateCouponTemplateCanTake(template, userIds, takeType); validateCouponTemplateCanTake(template, userIds, takeType);
// 3. 批量保存优惠劵 // 3. 批量保存优惠劵
couponMapper.insertBatch(convertList(userIds, userId -> CouponConvert.INSTANCE.convert(template, userId))); List<CouponDO> couponList = convertList(userIds, userId -> CouponConvert.INSTANCE.convert(template, userId));
couponMapper.insertBatch(couponList);
// 3. 增加优惠劵模板的领取数量 // 4. 增加优惠劵模板的领取数量
couponTemplateService.updateCouponTemplateTakeCount(templateId, userIds.size()); couponTemplateService.updateCouponTemplateTakeCount(templateId, userIds.size());
return convertMultiMap(couponList, CouponDO::getUserId, CouponDO::getId);
}
@Override
public List<Long> takeCouponsByAdmin(Map<Long, Integer> giveCoupons, Long userId) {
if (CollUtil.isEmpty(giveCoupons)) {
return Collections.emptyList();
}
List<Long> couponIds = new ArrayList<>();
// 循环发放
for (Map.Entry<Long, Integer> entry : giveCoupons.entrySet()) {
try {
for (int i = 0; i < entry.getValue(); i++) {
Map<Long, List<Long>> userCouponIdsMap = getSelf().takeCoupon(entry.getKey(), CollUtil.newHashSet(userId),
CouponTakeTypeEnum.ADMIN);
findAndThen(userCouponIdsMap, userId, couponIds::addAll);
}
} catch (Exception e) {
log.error("[takeCouponsByAdmin][coupon({}) 优惠券发放失败]", entry, e);
}
}
return couponIds;
}
@Override
public void invalidateCouponsByAdmin(List<Long> giveCouponIds, Long userId) {
// 循环收回
for (Long couponId : giveCouponIds) {
try {
getSelf().invalidateCoupon(couponId, userId);
} catch (Exception e) {
log.error("[invalidateCouponsByAdmin][couponId({}) 收回优惠券失败]", couponId, e);
}
}
}
/**
* 管理员收回优惠券
*
* @param couponId 模版编号
* @param userId 用户编号
*/
@Transactional(rollbackFor = Exception.class)
public void invalidateCoupon(Long couponId, Long userId) {
// 1.1 校验优惠券
CouponDO coupon = couponMapper.selectByIdAndUserId(couponId, userId);
if (coupon == null) {
throw exception(COUPON_NOT_EXISTS);
}
// 1.2 校验模板
CouponTemplateDO couponTemplate = couponTemplateService.getCouponTemplate(coupon.getTemplateId());
if (couponTemplate == null) {
throw exception(COUPON_TEMPLATE_NOT_EXISTS);
}
// 1.3 校验优惠券是否已经使用如若使用则先不管
if (ObjUtil.equal(coupon.getStatus(), CouponStatusEnum.USED.getStatus())) {
log.info("[invalidateCoupon][coupon({}) 已经使用,无法作废]", couponId);
return;
}
// 2.1 减少优惠劵模板的领取数量
couponTemplateService.updateCouponTemplateTakeCount(couponTemplate.getId(), -1);
// 2.2 作废优惠劵
couponMapper.deleteById(couponId);
} }
@Override @Override
@ -188,24 +225,6 @@ public class CouponServiceImpl implements CouponService {
} }
} }
@Override
public Map<Long, Integer> getTakeCountMapByTemplateIds(Collection<Long> templateIds, Long userId) {
if (CollUtil.isEmpty(templateIds)) {
return Collections.emptyMap();
}
return couponMapper.selectCountByUserIdAndTemplateIdIn(userId, templateIds);
}
@Override
public List<CouponDO> getMatchCouponList(Long userId, AppCouponMatchReqVO matchReqVO) {
List<CouponDO> list = couponMapper.selectListByUserIdAndStatusAndUsePriceLeAndProductScope(userId,
CouponStatusEnum.UNUSED.getStatus(),
matchReqVO.getPrice(), matchReqVO.getSpuIds(), matchReqVO.getCategoryIds());
// 兜底逻辑如果 CouponExpireJob 未执行status 未变成 EXPIRE 但是 validEndTime 已经过期了需要进行过滤
list.removeIf(coupon -> !LocalDateTimeUtils.isBetween(coupon.getValidStartTime(), coupon.getValidEndTime()));
return list;
}
@Override @Override
public int expireCoupon() { public int expireCoupon() {
// 1. 查询待过期的优惠券 // 1. 查询待过期的优惠券
@ -230,27 +249,6 @@ public class CouponServiceImpl implements CouponService {
return count; return count;
} }
@Override
public Map<Long, Boolean> getUserCanCanTakeMap(Long userId, List<CouponTemplateDO> templates) {
// 1. 未登录时都显示可以领取
Map<Long, Boolean> userCanTakeMap = convertMap(templates, CouponTemplateDO::getId, templateId -> true);
if (userId == null) {
return userCanTakeMap;
}
// 2.1 过滤领取数量无限制的
Set<Long> templateIds = convertSet(templates, CouponTemplateDO::getId, template -> template.getTakeLimitCount() != -1);
// 2.2 检查用户领取的数量是否超过限制
if (CollUtil.isNotEmpty(templateIds)) {
Map<Long, Integer> couponTakeCountMap = this.getTakeCountMapByTemplateIds(templateIds, userId);
for (CouponTemplateDO template : templates) {
Integer takeCount = couponTakeCountMap.get(template.getId());
userCanTakeMap.put(template.getId(), takeCount == null || takeCount < template.getTakeLimitCount());
}
}
return userCanTakeMap;
}
/** /**
* 过期单个优惠劵 * 过期单个优惠劵
* *
@ -322,11 +320,84 @@ public class CouponServiceImpl implements CouponService {
userIds.removeIf(userId -> MapUtil.getInt(userTakeCountMap, userId, 0) >= couponTemplate.getTakeLimitCount()); userIds.removeIf(userId -> MapUtil.getInt(userTakeCountMap, userId, 0) >= couponTemplate.getTakeLimitCount());
} }
//======================= 查询相关 =======================
@Override
public Long getUnusedCouponCount(Long userId) {
return couponMapper.selectCountByUserIdAndStatus(userId, CouponStatusEnum.UNUSED.getStatus());
}
@Override
public PageResult<CouponDO> getCouponPage(CouponPageReqVO pageReqVO) {
// 获得用户编号
if (StrUtil.isNotEmpty(pageReqVO.getNickname())) {
List<MemberUserRespDTO> users = memberUserApi.getUserListByNickname(pageReqVO.getNickname());
if (CollUtil.isEmpty(users)) {
return PageResult.empty();
}
pageReqVO.setUserIds(convertSet(users, MemberUserRespDTO::getId));
}
// 分页查询
return couponMapper.selectPage(pageReqVO);
}
@Override
public List<CouponDO> getCouponList(Long userId, Integer status) {
return couponMapper.selectListByUserIdAndStatus(userId, status);
}
@Override
public Map<Long, Integer> getTakeCountMapByTemplateIds(Collection<Long> templateIds, Long userId) {
if (CollUtil.isEmpty(templateIds)) {
return Collections.emptyMap();
}
return couponMapper.selectCountByUserIdAndTemplateIdIn(userId, templateIds);
}
@Override
public List<CouponDO> getMatchCouponList(Long userId, AppCouponMatchReqVO matchReqVO) {
List<CouponDO> list = couponMapper.selectListByUserIdAndStatusAndUsePriceLeAndProductScope(userId,
CouponStatusEnum.UNUSED.getStatus(),
matchReqVO.getPrice(), matchReqVO.getSpuIds(), matchReqVO.getCategoryIds());
// 兜底逻辑如果 CouponExpireJob 未执行status 未变成 EXPIRE 但是 validEndTime 已经过期了需要进行过滤
list.removeIf(coupon -> !LocalDateTimeUtils.isBetween(coupon.getValidStartTime(), coupon.getValidEndTime()));
return list;
}
@Override
public Map<Long, Boolean> getUserCanCanTakeMap(Long userId, List<CouponTemplateDO> templates) {
// 1. 未登录时都显示可以领取
Map<Long, Boolean> userCanTakeMap = convertMap(templates, CouponTemplateDO::getId, templateId -> true);
if (userId == null) {
return userCanTakeMap;
}
// 2.1 过滤领取数量无限制的
Set<Long> templateIds = convertSet(templates, CouponTemplateDO::getId, template -> template.getTakeLimitCount() != -1);
// 2.2 检查用户领取的数量是否超过限制
if (CollUtil.isNotEmpty(templateIds)) {
Map<Long, Integer> couponTakeCountMap = this.getTakeCountMapByTemplateIds(templateIds, userId);
for (CouponTemplateDO template : templates) {
Integer takeCount = couponTakeCountMap.get(template.getId());
userCanTakeMap.put(template.getId(), takeCount == null || takeCount < template.getTakeLimitCount());
}
}
return userCanTakeMap;
}
@Override @Override
public CouponDO getCoupon(Long userId, Long id) { public CouponDO getCoupon(Long userId, Long id) {
return couponMapper.selectByIdAndUserId(id, userId); return couponMapper.selectByIdAndUserId(id, userId);
} }
private CouponDO validateCouponExists(Long id) {
CouponDO coupon = couponMapper.selectById(id);
if (coupon == null) {
throw exception(COUPON_NOT_EXISTS);
}
return coupon;
}
/** /**
* 获得自身的代理对象解决 AOP 生效问题 * 获得自身的代理对象解决 AOP 生效问题
* *
@ -335,4 +406,5 @@ public class CouponServiceImpl implements CouponService {
private CouponServiceImpl getSelf() { private CouponServiceImpl getSelf() {
return SpringUtil.getBean(getClass()); return SpringUtil.getBean(getClass());
} }
} }

View File

@ -104,7 +104,10 @@ public class DiscountActivityServiceImpl implements DiscountActivityService {
} }
// 计算新增的记录 // 计算新增的记录
List<DiscountProductDO> newDiscountProducts = convertList(updateReqVO.getProducts(), List<DiscountProductDO> newDiscountProducts = convertList(updateReqVO.getProducts(),
product -> DiscountActivityConvert.INSTANCE.convert(product).setActivityId(updateReqVO.getId())); product -> DiscountActivityConvert.INSTANCE.convert(product)
.setActivityId(updateReqVO.getId())
.setActivityStartTime(updateReqVO.getStartTime())
.setActivityEndTime(updateReqVO.getEndTime()));
newDiscountProducts.removeIf(product -> dbDiscountProducts.stream().anyMatch( newDiscountProducts.removeIf(product -> dbDiscountProducts.stream().anyMatch(
dbProduct -> DiscountActivityConvert.INSTANCE.isEquals(dbProduct, product))); // 如果匹配到说明是更新的 dbProduct -> DiscountActivityConvert.INSTANCE.isEquals(dbProduct, product))); // 如果匹配到说明是更新的
if (CollectionUtil.isNotEmpty(newDiscountProducts)) { if (CollectionUtil.isNotEmpty(newDiscountProducts)) {

View File

@ -75,11 +75,10 @@ public interface RewardActivityService {
/** /**
* 获取指定 spu 编号最近参加的活动每个 spuId 只返回一条记录 * 获取指定 spu 编号最近参加的活动每个 spuId 只返回一条记录
* *
* @param spuIds spu 编号
* @param status 状态 * @param status 状态
* @param dateTime 当前日期时间 * @param dateTime 当前日期时间
* @return 满减送活动列表 * @return 满减送活动列表
*/ */
List<RewardActivityDO> getRewardActivityBySpuIdsAndStatusAndDateTimeLt(Collection<Long> spuIds, Integer status, LocalDateTime dateTime); List<RewardActivityDO> getRewardActivityListByStatusAndDateTimeLt(Integer status, LocalDateTime dateTime);
} }

View File

@ -1,15 +1,18 @@
package cn.iocoder.yudao.module.promotion.service.reward; package cn.iocoder.yudao.module.promotion.service.reward;
import cn.hutool.core.collection.CollUtil; import cn.iocoder.yudao.framework.common.enums.CommonStatusEnum;
import cn.iocoder.yudao.framework.common.pojo.PageResult; import cn.iocoder.yudao.framework.common.pojo.PageResult;
import cn.iocoder.yudao.framework.common.util.object.BeanUtils;
import cn.iocoder.yudao.module.product.api.category.ProductCategoryApi;
import cn.iocoder.yudao.module.product.api.spu.ProductSpuApi;
import cn.iocoder.yudao.module.promotion.api.reward.dto.RewardActivityMatchRespDTO; import cn.iocoder.yudao.module.promotion.api.reward.dto.RewardActivityMatchRespDTO;
import cn.iocoder.yudao.module.promotion.controller.admin.reward.vo.RewardActivityBaseVO;
import cn.iocoder.yudao.module.promotion.controller.admin.reward.vo.RewardActivityCreateReqVO; import cn.iocoder.yudao.module.promotion.controller.admin.reward.vo.RewardActivityCreateReqVO;
import cn.iocoder.yudao.module.promotion.controller.admin.reward.vo.RewardActivityPageReqVO; import cn.iocoder.yudao.module.promotion.controller.admin.reward.vo.RewardActivityPageReqVO;
import cn.iocoder.yudao.module.promotion.controller.admin.reward.vo.RewardActivityUpdateReqVO; import cn.iocoder.yudao.module.promotion.controller.admin.reward.vo.RewardActivityUpdateReqVO;
import cn.iocoder.yudao.module.promotion.convert.reward.RewardActivityConvert;
import cn.iocoder.yudao.module.promotion.dal.dataobject.reward.RewardActivityDO; import cn.iocoder.yudao.module.promotion.dal.dataobject.reward.RewardActivityDO;
import cn.iocoder.yudao.module.promotion.dal.mysql.reward.RewardActivityMapper; import cn.iocoder.yudao.module.promotion.dal.mysql.reward.RewardActivityMapper;
import cn.iocoder.yudao.module.promotion.enums.common.PromotionActivityStatusEnum; import cn.iocoder.yudao.module.promotion.enums.common.PromotionProductScopeEnum;
import cn.iocoder.yudao.module.promotion.util.PromotionUtils; import cn.iocoder.yudao.module.promotion.util.PromotionUtils;
import jakarta.annotation.Resource; import jakarta.annotation.Resource;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
@ -17,13 +20,13 @@ import org.springframework.validation.annotation.Validated;
import java.time.LocalDateTime; import java.time.LocalDateTime;
import java.util.Collection; import java.util.Collection;
import java.util.Collections;
import java.util.List; import java.util.List;
import java.util.Objects;
import static cn.hutool.core.collection.CollUtil.intersectionDistinct;
import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception; import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception;
import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertSet; import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.anyMatch;
import static cn.iocoder.yudao.module.promotion.enums.ErrorCodeConstants.*; import static cn.iocoder.yudao.module.promotion.enums.ErrorCodeConstants.*;
import static java.util.Arrays.asList;
/** /**
* 满减送活动 Service 实现类 * 满减送活动 Service 实现类
@ -37,13 +40,20 @@ public class RewardActivityServiceImpl implements RewardActivityService {
@Resource @Resource
private RewardActivityMapper rewardActivityMapper; private RewardActivityMapper rewardActivityMapper;
@Resource
private ProductCategoryApi productCategoryApi;
@Resource
private ProductSpuApi productSpuApi;
@Override @Override
public Long createRewardActivity(RewardActivityCreateReqVO createReqVO) { public Long createRewardActivity(RewardActivityCreateReqVO createReqVO) {
// 校验商品是否冲突 // 1.1 校验商品范围
validateRewardActivitySpuConflicts(null, createReqVO.getProductSpuIds()); validateProductScope(createReqVO.getProductScope(), createReqVO.getProductScopeValues());
// 1.2 校验商品是否冲突
validateRewardActivitySpuConflicts(null, createReqVO);
// 插入 // 2. 插入
RewardActivityDO rewardActivity = RewardActivityConvert.INSTANCE.convert(createReqVO) RewardActivityDO rewardActivity = BeanUtils.toBean(createReqVO, RewardActivityDO.class)
.setStatus(PromotionUtils.calculateActivityStatus(createReqVO.getEndTime())); .setStatus(PromotionUtils.calculateActivityStatus(createReqVO.getEndTime()));
rewardActivityMapper.insert(rewardActivity); rewardActivityMapper.insert(rewardActivity);
// 返回 // 返回
@ -52,16 +62,18 @@ public class RewardActivityServiceImpl implements RewardActivityService {
@Override @Override
public void updateRewardActivity(RewardActivityUpdateReqVO updateReqVO) { public void updateRewardActivity(RewardActivityUpdateReqVO updateReqVO) {
// 校验存在 // 1.1 校验存在
RewardActivityDO dbRewardActivity = validateRewardActivityExists(updateReqVO.getId()); RewardActivityDO dbRewardActivity = validateRewardActivityExists(updateReqVO.getId());
if (dbRewardActivity.getStatus().equals(PromotionActivityStatusEnum.CLOSE.getStatus())) { // 已关闭的活动不能修改噢 if (dbRewardActivity.getStatus().equals(CommonStatusEnum.DISABLE.getStatus())) { // 已关闭的活动不能修改噢
throw exception(REWARD_ACTIVITY_UPDATE_FAIL_STATUS_CLOSED); throw exception(REWARD_ACTIVITY_UPDATE_FAIL_STATUS_CLOSED);
} }
// 校验商品是否冲突 // 1.2 校验商品范围
validateRewardActivitySpuConflicts(updateReqVO.getId(), updateReqVO.getProductSpuIds()); validateProductScope(updateReqVO.getProductScope(), updateReqVO.getProductScopeValues());
// 1.3 校验商品是否冲突
validateRewardActivitySpuConflicts(updateReqVO.getId(), updateReqVO);
// 更新 // 2. 更新
RewardActivityDO updateObj = RewardActivityConvert.INSTANCE.convert(updateReqVO) RewardActivityDO updateObj = BeanUtils.toBean(updateReqVO, RewardActivityDO.class)
.setStatus(PromotionUtils.calculateActivityStatus(updateReqVO.getEndTime())); .setStatus(PromotionUtils.calculateActivityStatus(updateReqVO.getEndTime()));
rewardActivityMapper.updateById(updateObj); rewardActivityMapper.updateById(updateObj);
} }
@ -70,15 +82,12 @@ public class RewardActivityServiceImpl implements RewardActivityService {
public void closeRewardActivity(Long id) { public void closeRewardActivity(Long id) {
// 校验存在 // 校验存在
RewardActivityDO dbRewardActivity = validateRewardActivityExists(id); RewardActivityDO dbRewardActivity = validateRewardActivityExists(id);
if (dbRewardActivity.getStatus().equals(PromotionActivityStatusEnum.CLOSE.getStatus())) { // 已关闭的活动不能关闭噢 if (dbRewardActivity.getStatus().equals(CommonStatusEnum.DISABLE.getStatus())) { // 已关闭的活动不能关闭噢
throw exception(REWARD_ACTIVITY_CLOSE_FAIL_STATUS_CLOSED); throw exception(REWARD_ACTIVITY_CLOSE_FAIL_STATUS_CLOSED);
} }
if (dbRewardActivity.getStatus().equals(PromotionActivityStatusEnum.END.getStatus())) { // 已关闭的活动不能关闭噢
throw exception(REWARD_ACTIVITY_CLOSE_FAIL_STATUS_END);
}
// 更新 // 更新
RewardActivityDO updateObj = new RewardActivityDO().setId(id).setStatus(PromotionActivityStatusEnum.CLOSE.getStatus()); RewardActivityDO updateObj = new RewardActivityDO().setId(id).setStatus(CommonStatusEnum.DISABLE.getStatus());
rewardActivityMapper.updateById(updateObj); rewardActivityMapper.updateById(updateObj);
} }
@ -86,7 +95,7 @@ public class RewardActivityServiceImpl implements RewardActivityService {
public void deleteRewardActivity(Long id) { public void deleteRewardActivity(Long id) {
// 校验存在 // 校验存在
RewardActivityDO dbRewardActivity = validateRewardActivityExists(id); RewardActivityDO dbRewardActivity = validateRewardActivityExists(id);
if (!dbRewardActivity.getStatus().equals(PromotionActivityStatusEnum.CLOSE.getStatus())) { // 未关闭的活动不能删除噢 if (dbRewardActivity.getStatus().equals(CommonStatusEnum.ENABLE.getStatus())) { // 未关闭的活动不能删除噢
throw exception(REWARD_ACTIVITY_DELETE_FAIL_STATUS_NOT_CLOSED); throw exception(REWARD_ACTIVITY_DELETE_FAIL_STATUS_NOT_CLOSED);
} }
@ -102,41 +111,39 @@ public class RewardActivityServiceImpl implements RewardActivityService {
return activity; return activity;
} }
// TODO @芋艿逻辑有问题需要优化要分成全场和指定来校验
/** /**
* 校验商品参加的活动是否冲突 * 校验商品参加的活动是否冲突
* *
* @param id 活动编号 * @param id 活动编号
* @param spuIds 商品 SPU 编号数组 * @param rewardActivity 请求
*/ */
private void validateRewardActivitySpuConflicts(Long id, Collection<Long> spuIds) { private void validateRewardActivitySpuConflicts(Long id, RewardActivityBaseVO rewardActivity) {
if (CollUtil.isEmpty(spuIds)) { List<RewardActivityDO> list = rewardActivityMapper.selectList(RewardActivityDO::getProductScope,
return; rewardActivity.getProductScope(), RewardActivityDO::getStatus, CommonStatusEnum.ENABLE.getStatus());
}
// 查询商品参加的活动
List<RewardActivityDO> rewardActivityList = getRewardActivityListBySpuIds(spuIds,
asList(PromotionActivityStatusEnum.WAIT.getStatus(), PromotionActivityStatusEnum.RUN.getStatus()));
if (id != null) { // 排除自己这个活动 if (id != null) { // 排除自己这个活动
rewardActivityList.removeIf(activity -> id.equals(activity.getId())); list.removeIf(activity -> id.equals(activity.getId()));
} }
// 如果非空则说明冲突
if (CollUtil.isNotEmpty(rewardActivityList)) { // 情况一全部商品参加
throw exception(REWARD_ACTIVITY_SPU_CONFLICTS); if (PromotionProductScopeEnum.isAll(rewardActivity.getProductScope()) && !list.isEmpty()) {
throw exception(REWARD_ACTIVITY_SCOPE_ALL_EXISTS);
}
if (PromotionProductScopeEnum.isSpu(rewardActivity.getProductScope()) || // 情况二指定商品参加
PromotionProductScopeEnum.isCategory(rewardActivity.getProductScope())) { // 情况三指定商品类型参加
if (anyMatch(list, item -> !intersectionDistinct(item.getProductScopeValues(),
rewardActivity.getProductScopeValues()).isEmpty())) {
throw exception(PromotionProductScopeEnum.isSpu(rewardActivity.getProductScope()) ?
REWARD_ACTIVITY_SPU_CONFLICTS : REWARD_ACTIVITY_SCOPE_CATEGORY_EXISTS);
}
} }
} }
/** private void validateProductScope(Integer productScope, List<Long> productScopeValues) {
* 获得商品参加的满减送活动的数组 if (Objects.equals(PromotionProductScopeEnum.SPU.getScope(), productScope)) {
* productSpuApi.validateSpuList(productScopeValues);
* @param spuIds 商品 SPU 编号数组 } else if (Objects.equals(PromotionProductScopeEnum.CATEGORY.getScope(), productScope)) {
* @param statuses 活动状态数组 productCategoryApi.validateCategoryList(productScopeValues);
* @return 商品参加的满减送活动的数组 }
*/
private List<RewardActivityDO> getRewardActivityListBySpuIds(Collection<Long> spuIds,
Collection<Integer> statuses) {
List<RewardActivityDO> list = rewardActivityMapper.selectListByStatus(statuses);
return CollUtil.filter(list, activity -> CollUtil.containsAny(activity.getProductSpuIds(), spuIds));
} }
@Override @Override
@ -151,32 +158,13 @@ public class RewardActivityServiceImpl implements RewardActivityService {
@Override @Override
public List<RewardActivityMatchRespDTO> getMatchRewardActivityList(Collection<Long> spuIds) { public List<RewardActivityMatchRespDTO> getMatchRewardActivityList(Collection<Long> spuIds) {
// TODO 芋艿待实现先指定然后再全局的 List<RewardActivityDO> list = rewardActivityMapper.selectListBySpuIdsAndStatus(spuIds, CommonStatusEnum.ENABLE.getStatus());
// // 如果有全局活动则直接选择它 return BeanUtils.toBean(list, RewardActivityMatchRespDTO.class);
// List<RewardActivityDO> allActivities = rewardActivityMapper.selectListByProductScopeAndStatus(
// PromotionProductScopeEnum.ALL.getScope(), PromotionActivityStatusEnum.RUN.getStatus());
// if (CollUtil.isNotEmpty(allActivities)) {
// return MapUtil.builder(allActivities.get(0), spuIds).build();
// }
//
// // 查询某个活动参加的活动
// List<RewardActivityDO> productActivityList = getRewardActivityListBySpuIds(spuIds,
// singleton(PromotionActivityStatusEnum.RUN.getStatus()));
// return convertMap(productActivityList, activity -> activity,
// rewardActivityDO -> intersectionDistinct(rewardActivityDO.getProductSpuIds(), spuIds)); // 求交集返回
return null;
} }
@Override @Override
public List<RewardActivityDO> getRewardActivityBySpuIdsAndStatusAndDateTimeLt(Collection<Long> spuIds, Integer status, LocalDateTime dateTime) { public List<RewardActivityDO> getRewardActivityListByStatusAndDateTimeLt(Integer status, LocalDateTime dateTime) {
// 1. 查询出指定 spuId spu 参加的活动 return rewardActivityMapper.selectListByStatusAndDateTimeLt(status, dateTime);
List<RewardActivityDO> rewardActivityList = rewardActivityMapper.selectListBySpuIdsAndStatus(spuIds, status);
if (CollUtil.isEmpty(rewardActivityList)) {
return Collections.emptyList();
}
// 2. 查询活动详情
return rewardActivityMapper.selectListByIdsAndDateTimeLt(convertSet(rewardActivityList, RewardActivityDO::getId), dateTime);
} }
} }

View File

@ -1,21 +1,23 @@
package cn.iocoder.yudao.module.promotion.service.reward; package cn.iocoder.yudao.module.promotion.service.reward;
import cn.iocoder.yudao.framework.common.enums.CommonStatusEnum;
import cn.iocoder.yudao.framework.common.pojo.PageResult; import cn.iocoder.yudao.framework.common.pojo.PageResult;
import cn.iocoder.yudao.framework.test.core.ut.BaseDbUnitTest; import cn.iocoder.yudao.framework.test.core.ut.BaseDbUnitTest;
import cn.iocoder.yudao.module.promotion.api.reward.dto.RewardActivityMatchRespDTO;
import cn.iocoder.yudao.module.promotion.controller.admin.reward.vo.RewardActivityCreateReqVO; import cn.iocoder.yudao.module.promotion.controller.admin.reward.vo.RewardActivityCreateReqVO;
import cn.iocoder.yudao.module.promotion.controller.admin.reward.vo.RewardActivityPageReqVO; import cn.iocoder.yudao.module.promotion.controller.admin.reward.vo.RewardActivityPageReqVO;
import cn.iocoder.yudao.module.promotion.controller.admin.reward.vo.RewardActivityUpdateReqVO; import cn.iocoder.yudao.module.promotion.controller.admin.reward.vo.RewardActivityUpdateReqVO;
import cn.iocoder.yudao.module.promotion.dal.dataobject.reward.RewardActivityDO; import cn.iocoder.yudao.module.promotion.dal.dataobject.reward.RewardActivityDO;
import cn.iocoder.yudao.module.promotion.dal.mysql.reward.RewardActivityMapper; import cn.iocoder.yudao.module.promotion.dal.mysql.reward.RewardActivityMapper;
import cn.iocoder.yudao.module.promotion.enums.common.PromotionActivityStatusEnum;
import cn.iocoder.yudao.module.promotion.enums.common.PromotionConditionTypeEnum; import cn.iocoder.yudao.module.promotion.enums.common.PromotionConditionTypeEnum;
import cn.iocoder.yudao.module.promotion.enums.common.PromotionProductScopeEnum; import cn.iocoder.yudao.module.promotion.enums.common.PromotionProductScopeEnum;
import jakarta.annotation.Resource;
import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.Disabled;
import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Test;
import org.springframework.context.annotation.Import; import org.springframework.context.annotation.Import;
import jakarta.annotation.Resource;
import java.time.Duration; import java.time.Duration;
import java.util.List;
import java.util.Set; import java.util.Set;
import static cn.hutool.core.util.RandomUtil.randomEle; import static cn.hutool.core.util.RandomUtil.randomEle;
@ -27,15 +29,15 @@ import static cn.iocoder.yudao.framework.test.core.util.AssertUtils.assertServic
import static cn.iocoder.yudao.framework.test.core.util.RandomUtils.randomLongId; import static cn.iocoder.yudao.framework.test.core.util.RandomUtils.randomLongId;
import static cn.iocoder.yudao.framework.test.core.util.RandomUtils.randomPojo; import static cn.iocoder.yudao.framework.test.core.util.RandomUtils.randomPojo;
import static cn.iocoder.yudao.module.promotion.enums.ErrorCodeConstants.REWARD_ACTIVITY_NOT_EXISTS; import static cn.iocoder.yudao.module.promotion.enums.ErrorCodeConstants.REWARD_ACTIVITY_NOT_EXISTS;
import static java.util.Arrays.asList; import static com.google.common.primitives.Longs.asList;
import static java.util.Collections.singletonList; import static java.util.Collections.singletonList;
import static org.junit.jupiter.api.Assertions.*; import static org.junit.jupiter.api.Assertions.*;
/** /**
* {@link RewardActivityServiceImpl} 的单元测试类 * {@link RewardActivityServiceImpl} 的单元测试类
* *
* @author 芋道源码 * @author 芋道源码
*/ */
@Disabled // TODO 芋艿后续 fix 补充的单测 @Disabled // TODO 芋艿后续 fix 补充的单测
@Import(RewardActivityServiceImpl.class) @Import(RewardActivityServiceImpl.class)
public class RewardActivityServiceImplTest extends BaseDbUnitTest { public class RewardActivityServiceImplTest extends BaseDbUnitTest {
@ -63,7 +65,7 @@ public class RewardActivityServiceImplTest extends BaseDbUnitTest {
// 校验记录的属性是否正确 // 校验记录的属性是否正确
RewardActivityDO rewardActivity = rewardActivityMapper.selectById(rewardActivityId); RewardActivityDO rewardActivity = rewardActivityMapper.selectById(rewardActivityId);
assertPojoEquals(reqVO, rewardActivity, "rules"); assertPojoEquals(reqVO, rewardActivity, "rules");
assertEquals(rewardActivity.getStatus(), PromotionActivityStatusEnum.WAIT.getStatus()); assertEquals(rewardActivity.getStatus(), CommonStatusEnum.DISABLE.getStatus());
for (int i = 0; i < reqVO.getRules().size(); i++) { for (int i = 0; i < reqVO.getRules().size(); i++) {
assertPojoEquals(reqVO.getRules().get(i), rewardActivity.getRules().get(i)); assertPojoEquals(reqVO.getRules().get(i), rewardActivity.getRules().get(i));
} }
@ -72,7 +74,7 @@ public class RewardActivityServiceImplTest extends BaseDbUnitTest {
@Test @Test
public void testUpdateRewardActivity_success() { public void testUpdateRewardActivity_success() {
// mock 数据 // mock 数据
RewardActivityDO dbRewardActivity = randomPojo(RewardActivityDO.class, o -> o.setStatus(PromotionActivityStatusEnum.WAIT.getStatus())); RewardActivityDO dbRewardActivity = randomPojo(RewardActivityDO.class, o -> o.setStatus(CommonStatusEnum.DISABLE.getStatus()));
rewardActivityMapper.insert(dbRewardActivity);// @Sql: 先插入出一条存在的数据 rewardActivityMapper.insert(dbRewardActivity);// @Sql: 先插入出一条存在的数据
// 准备参数 // 准备参数
RewardActivityUpdateReqVO reqVO = randomPojo(RewardActivityUpdateReqVO.class, o -> { RewardActivityUpdateReqVO reqVO = randomPojo(RewardActivityUpdateReqVO.class, o -> {
@ -88,7 +90,7 @@ public class RewardActivityServiceImplTest extends BaseDbUnitTest {
// 校验是否更新正确 // 校验是否更新正确
RewardActivityDO rewardActivity = rewardActivityMapper.selectById(reqVO.getId()); // 获取最新的 RewardActivityDO rewardActivity = rewardActivityMapper.selectById(reqVO.getId()); // 获取最新的
assertPojoEquals(reqVO, rewardActivity, "rules"); assertPojoEquals(reqVO, rewardActivity, "rules");
assertEquals(rewardActivity.getStatus(), PromotionActivityStatusEnum.WAIT.getStatus()); assertEquals(rewardActivity.getStatus(), CommonStatusEnum.DISABLE.getStatus());
for (int i = 0; i < reqVO.getRules().size(); i++) { for (int i = 0; i < reqVO.getRules().size(); i++) {
assertPojoEquals(reqVO.getRules().get(i), rewardActivity.getRules().get(i)); assertPojoEquals(reqVO.getRules().get(i), rewardActivity.getRules().get(i));
} }
@ -97,7 +99,7 @@ public class RewardActivityServiceImplTest extends BaseDbUnitTest {
@Test @Test
public void testCloseRewardActivity() { public void testCloseRewardActivity() {
// mock 数据 // mock 数据
RewardActivityDO dbRewardActivity = randomPojo(RewardActivityDO.class, o -> o.setStatus(PromotionActivityStatusEnum.WAIT.getStatus())); RewardActivityDO dbRewardActivity = randomPojo(RewardActivityDO.class, o -> o.setStatus(CommonStatusEnum.DISABLE.getStatus()));
rewardActivityMapper.insert(dbRewardActivity);// @Sql: 先插入出一条存在的数据 rewardActivityMapper.insert(dbRewardActivity);// @Sql: 先插入出一条存在的数据
// 准备参数 // 准备参数
Long id = dbRewardActivity.getId(); Long id = dbRewardActivity.getId();
@ -106,7 +108,7 @@ public class RewardActivityServiceImplTest extends BaseDbUnitTest {
rewardActivityService.closeRewardActivity(id); rewardActivityService.closeRewardActivity(id);
// 校验状态 // 校验状态
RewardActivityDO rewardActivity = rewardActivityMapper.selectById(id); RewardActivityDO rewardActivity = rewardActivityMapper.selectById(id);
assertEquals(rewardActivity.getStatus(), PromotionActivityStatusEnum.CLOSE.getStatus()); assertEquals(rewardActivity.getStatus(), CommonStatusEnum.DISABLE.getStatus());
} }
@Test @Test
@ -121,15 +123,15 @@ public class RewardActivityServiceImplTest extends BaseDbUnitTest {
@Test @Test
public void testDeleteRewardActivity_success() { public void testDeleteRewardActivity_success() {
// mock 数据 // mock 数据
RewardActivityDO dbRewardActivity = randomPojo(RewardActivityDO.class, o -> o.setStatus(PromotionActivityStatusEnum.CLOSE.getStatus())); RewardActivityDO dbRewardActivity = randomPojo(RewardActivityDO.class, o -> o.setStatus(CommonStatusEnum.DISABLE.getStatus()));
rewardActivityMapper.insert(dbRewardActivity);// @Sql: 先插入出一条存在的数据 rewardActivityMapper.insert(dbRewardActivity);// @Sql: 先插入出一条存在的数据
// 准备参数 // 准备参数
Long id = dbRewardActivity.getId(); Long id = dbRewardActivity.getId();
// 调用 // 调用
rewardActivityService.deleteRewardActivity(id); rewardActivityService.deleteRewardActivity(id);
// 校验数据不存在了 // 校验数据不存在了
assertNull(rewardActivityMapper.selectById(id)); assertNull(rewardActivityMapper.selectById(id));
} }
@Test @Test
@ -143,77 +145,82 @@ public class RewardActivityServiceImplTest extends BaseDbUnitTest {
@Test @Test
public void testGetRewardActivityPage() { public void testGetRewardActivityPage() {
// mock 数据 // mock 数据
RewardActivityDO dbRewardActivity = randomPojo(RewardActivityDO.class, o -> { // 等会查询到 RewardActivityDO dbRewardActivity = randomPojo(RewardActivityDO.class, o -> { // 等会查询到
o.setName("芋艿"); o.setName("芋艿");
o.setStatus(PromotionActivityStatusEnum.CLOSE.getStatus()); o.setStatus(CommonStatusEnum.DISABLE.getStatus());
}); });
rewardActivityMapper.insert(dbRewardActivity); rewardActivityMapper.insert(dbRewardActivity);
// 测试 name 不匹配 // 测试 name 不匹配
rewardActivityMapper.insert(cloneIgnoreId(dbRewardActivity, o -> o.setName("土豆"))); rewardActivityMapper.insert(cloneIgnoreId(dbRewardActivity, o -> o.setName("土豆")));
// 测试 status 不匹配 // 测试 status 不匹配
rewardActivityMapper.insert(cloneIgnoreId(dbRewardActivity, o -> o.setStatus(PromotionActivityStatusEnum.RUN.getStatus()))); rewardActivityMapper.insert(cloneIgnoreId(dbRewardActivity, o -> o.setStatus(CommonStatusEnum.ENABLE.getStatus())));
// 准备参数 // 准备参数
RewardActivityPageReqVO reqVO = new RewardActivityPageReqVO(); RewardActivityPageReqVO reqVO = new RewardActivityPageReqVO();
reqVO.setName("芋艿"); reqVO.setName("芋艿");
reqVO.setStatus(PromotionActivityStatusEnum.CLOSE.getStatus()); reqVO.setStatus(CommonStatusEnum.DISABLE.getStatus());
// 调用 // 调用
PageResult<RewardActivityDO> pageResult = rewardActivityService.getRewardActivityPage(reqVO); PageResult<RewardActivityDO> pageResult = rewardActivityService.getRewardActivityPage(reqVO);
// 断言 // 断言
assertEquals(1, pageResult.getTotal()); assertEquals(1, pageResult.getTotal());
assertEquals(1, pageResult.getList().size()); assertEquals(1, pageResult.getList().size());
assertPojoEquals(dbRewardActivity, pageResult.getList().get(0), "rules"); assertPojoEquals(dbRewardActivity, pageResult.getList().get(0), "rules");
} }
@Test @Test
public void testGetRewardActivities_all() { public void testGetRewardActivities_all() {
// mock 数据 // mock 数据
RewardActivityDO allActivity = randomPojo(RewardActivityDO.class, o -> o.setStatus(PromotionActivityStatusEnum.RUN.getStatus()) RewardActivityDO allActivity = randomPojo(RewardActivityDO.class, o -> o.setStatus(CommonStatusEnum.ENABLE.getStatus())
.setProductScope(PromotionProductScopeEnum.ALL.getScope())); .setProductScope(PromotionProductScopeEnum.ALL.getScope()));
rewardActivityMapper.insert(allActivity); rewardActivityMapper.insert(allActivity);
RewardActivityDO productActivity = randomPojo(RewardActivityDO.class, o -> o.setStatus(PromotionActivityStatusEnum.RUN.getStatus()) RewardActivityDO productActivity = randomPojo(RewardActivityDO.class, o -> o.setStatus(CommonStatusEnum.ENABLE.getStatus())
.setProductScope(PromotionProductScopeEnum.SPU.getScope()).setProductSpuIds(asList(1L, 2L))); .setProductScope(PromotionProductScopeEnum.SPU.getScope()).setProductScopeValues(asList(1L, 2L)));
rewardActivityMapper.insert(productActivity); rewardActivityMapper.insert(productActivity);
// 准备参数 // 准备参数
Set<Long> spuIds = asSet(1L, 2L); Set<Long> spuIds = asSet(1L, 2L);
// 调用 TODO getMatchRewardActivities 没有这个方法但是找到了 getMatchRewardActivityList // 调用 TODO getMatchRewardActivities 没有这个方法但是找到了 getMatchRewardActivityList
//Map<RewardActivityDO, Set<Long>> matchRewardActivities = rewardActivityService.getMatchRewardActivities(spuIds); List<RewardActivityMatchRespDTO> matchRewardActivityList = rewardActivityService.getMatchRewardActivityList(spuIds);
// 断言 // 断言
//assertEquals(matchRewardActivities.size(), 1); assertEquals(matchRewardActivityList.size(), 1);
//Map.Entry<RewardActivityDO, Set<Long>> next = matchRewardActivities.entrySet().iterator().next(); matchRewardActivityList.forEach((activity) -> {
//assertPojoEquals(next.getKey(), allActivity); if (activity.getId().equals(productActivity.getId())) {
//assertEquals(next.getValue(), spuIds); assertPojoEquals(activity, productActivity);
assertEquals(activity.getProductScopeValues(), asList(1L, 2L));
} else {
fail();
}
});
} }
@Test @Test
public void testGetRewardActivities_product() { public void testGetRewardActivities_product() {
// mock 数据 // mock 数据
RewardActivityDO productActivity01 = randomPojo(RewardActivityDO.class, o -> o.setStatus(PromotionActivityStatusEnum.RUN.getStatus()) RewardActivityDO productActivity01 = randomPojo(RewardActivityDO.class, o -> o.setStatus(CommonStatusEnum.ENABLE.getStatus())
.setProductScope(PromotionProductScopeEnum.SPU.getScope()).setProductSpuIds(asList(1L, 2L))); .setProductScope(PromotionProductScopeEnum.SPU.getScope()).setProductScopeValues(asList(1L, 2L)));
rewardActivityMapper.insert(productActivity01); rewardActivityMapper.insert(productActivity01);
RewardActivityDO productActivity02 = randomPojo(RewardActivityDO.class, o -> o.setStatus(PromotionActivityStatusEnum.RUN.getStatus()) RewardActivityDO productActivity02 = randomPojo(RewardActivityDO.class, o -> o.setStatus(CommonStatusEnum.ENABLE.getStatus())
.setProductScope(PromotionProductScopeEnum.SPU.getScope()).setProductSpuIds(singletonList(3L))); .setProductScope(PromotionProductScopeEnum.SPU.getScope()).setProductScopeValues(singletonList(3L)));
rewardActivityMapper.insert(productActivity02); rewardActivityMapper.insert(productActivity02);
// 准备参数 // 准备参数
Set<Long> spuIds = asSet(1L, 2L, 3L); Set<Long> spuIds = asSet(1L, 2L, 3L);
// 调用 TODO getMatchRewardActivities 没有这个方法但是找到了 getMatchRewardActivityList // 调用 TODO getMatchRewardActivities 没有这个方法但是找到了 getMatchRewardActivityList
//Map<RewardActivityDO, Set<Long>> matchRewardActivities = rewardActivityService.getMatchRewardActivities(spuIds); List<RewardActivityMatchRespDTO> matchRewardActivityList = rewardActivityService.getMatchRewardActivityList(spuIds);
// 断言 // 断言
//assertEquals(matchRewardActivities.size(), 2); assertEquals(matchRewardActivityList.size(), 2);
//matchRewardActivities.forEach((activity, activitySpuIds) -> { matchRewardActivityList.forEach((activity) -> {
// if (activity.getId().equals(productActivity01.getId())) { if (activity.getId().equals(productActivity01.getId())) {
// assertPojoEquals(activity, productActivity01); assertPojoEquals(activity, productActivity01);
// assertEquals(activitySpuIds, asSet(1L, 2L)); assertEquals(activity.getProductScopeValues(), asList(1L, 2L));
// } else if (activity.getId().equals(productActivity02.getId())) { } else if (activity.getId().equals(productActivity02.getId())) {
// assertPojoEquals(activity, productActivity02); assertPojoEquals(activity, productActivity02);
// assertEquals(activitySpuIds, asSet(3L)); assertEquals(activity.getProductScopeValues(), singletonList(3L));
// } else { } else {
// fail(); fail();
// } }
//}); });
} }
} }

View File

@ -28,13 +28,13 @@ public interface TradeOrderApi {
*/ */
TradeOrderRespDTO getOrder(Long id); TradeOrderRespDTO getOrder(Long id);
// TODO 芋艿需要优化下
/** /**
* 取消支付订单 * 取消支付订单
* *
* @param userId 用户编号 * @param userId 用户编号
* @param orderId 订单编号 * @param orderId 订单编号
* @param cancelType 取消类型
*/ */
void cancelPaidOrder(Long userId, Long orderId); void cancelPaidOrder(Long userId, Long orderId, Integer cancelType);
} }

View File

@ -35,6 +35,7 @@ public interface ErrorCodeConstants {
ErrorCode ORDER_RECEIVE_FAIL_DELIVERY_TYPE_NOT_PICK_UP = new ErrorCode(1_011_000_030, "交易订单自提失败,收货方式不是【用户自提】"); ErrorCode ORDER_RECEIVE_FAIL_DELIVERY_TYPE_NOT_PICK_UP = new ErrorCode(1_011_000_030, "交易订单自提失败,收货方式不是【用户自提】");
ErrorCode ORDER_UPDATE_ADDRESS_FAIL_STATUS_NOT_DELIVERED = new ErrorCode(1_011_000_031, "交易订单修改收货地址失败,原因:订单不是【待发货】状态"); ErrorCode ORDER_UPDATE_ADDRESS_FAIL_STATUS_NOT_DELIVERED = new ErrorCode(1_011_000_031, "交易订单修改收货地址失败,原因:订单不是【待发货】状态");
ErrorCode ORDER_CREATE_FAIL_EXIST_UNPAID = new ErrorCode(1_011_000_032, "交易订单创建失败,原因:存在未付款订单"); ErrorCode ORDER_CREATE_FAIL_EXIST_UNPAID = new ErrorCode(1_011_000_032, "交易订单创建失败,原因:存在未付款订单");
ErrorCode ORDER_CANCEL_PAID_FAIL = new ErrorCode(1_011_000_033, "交易订单取消支付失败,原因:订单不是【{}】状态");
// ========== After Sale 模块 1-011-000-100 ========== // ========== After Sale 模块 1-011-000-100 ==========
ErrorCode AFTER_SALE_NOT_FOUND = new ErrorCode(1_011_000_100, "售后单不存在"); ErrorCode AFTER_SALE_NOT_FOUND = new ErrorCode(1_011_000_100, "售后单不存在");
@ -59,6 +60,8 @@ public interface ErrorCodeConstants {
ErrorCode PRICE_CALCULATE_DELIVERY_PRICE_TEMPLATE_NOT_FOUND = new ErrorCode(1_011_003_002, "计算快递运费异常,找不到对应的运费模板"); ErrorCode PRICE_CALCULATE_DELIVERY_PRICE_TEMPLATE_NOT_FOUND = new ErrorCode(1_011_003_002, "计算快递运费异常,找不到对应的运费模板");
ErrorCode PRICE_CALCULATE_COUPON_NOT_MATCH_NORMAL_ORDER = new ErrorCode(1_011_003_004, "参与秒杀、拼团、砍价的营销商品,无法使用优惠劵"); ErrorCode PRICE_CALCULATE_COUPON_NOT_MATCH_NORMAL_ORDER = new ErrorCode(1_011_003_004, "参与秒杀、拼团、砍价的营销商品,无法使用优惠劵");
ErrorCode PRICE_CALCULATE_SECKILL_TOTAL_LIMIT_COUNT = new ErrorCode(1_011_003_005, "参与秒杀的商品,超过了秒杀总限购数量"); ErrorCode PRICE_CALCULATE_SECKILL_TOTAL_LIMIT_COUNT = new ErrorCode(1_011_003_005, "参与秒杀的商品,超过了秒杀总限购数量");
ErrorCode PRICE_CALCULATE_DELIVERY_PRICE_TYPE_ILLEGAL = new ErrorCode(1_011_003_006, "计算快递运费异常,配送方式不匹配");
ErrorCode PRICE_CALCULATE_COUPON_PRICE_TOO_MUCH = new ErrorCode(1_011_003_007, "该优惠劵无法使用,原因:优惠金额超过订单金额");
// ========== 物流 Express 模块 1-011-004-000 ========== // ========== 物流 Express 模块 1-011-004-000 ==========
ErrorCode EXPRESS_NOT_EXISTS = new ErrorCode(1_011_004_000, "快递公司不存在"); ErrorCode EXPRESS_NOT_EXISTS = new ErrorCode(1_011_004_000, "快递公司不存在");

View File

@ -17,7 +17,8 @@ public enum TradeOrderCancelTypeEnum implements IntArrayValuable {
PAY_TIMEOUT(10, "超时未支付"), PAY_TIMEOUT(10, "超时未支付"),
AFTER_SALE_CLOSE(20, "退款关闭"), AFTER_SALE_CLOSE(20, "退款关闭"),
MEMBER_CANCEL(30, "买家取消"); MEMBER_CANCEL(30, "买家取消"),
COMBINATION_CLOSE(40, "拼团关闭");
public static final int[] ARRAYS = Arrays.stream(values()).mapToInt(TradeOrderCancelTypeEnum::getType).toArray(); public static final int[] ARRAYS = Arrays.stream(values()).mapToInt(TradeOrderCancelTypeEnum::getType).toArray();

View File

@ -4,10 +4,10 @@ import cn.iocoder.yudao.module.trade.api.order.dto.TradeOrderRespDTO;
import cn.iocoder.yudao.module.trade.convert.order.TradeOrderConvert; import cn.iocoder.yudao.module.trade.convert.order.TradeOrderConvert;
import cn.iocoder.yudao.module.trade.service.order.TradeOrderQueryService; import cn.iocoder.yudao.module.trade.service.order.TradeOrderQueryService;
import cn.iocoder.yudao.module.trade.service.order.TradeOrderUpdateService; import cn.iocoder.yudao.module.trade.service.order.TradeOrderUpdateService;
import jakarta.annotation.Resource;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
import org.springframework.validation.annotation.Validated; import org.springframework.validation.annotation.Validated;
import jakarta.annotation.Resource;
import java.util.Collection; import java.util.Collection;
import java.util.List; import java.util.List;
@ -36,8 +36,8 @@ public class TradeOrderApiImpl implements TradeOrderApi {
} }
@Override @Override
public void cancelPaidOrder(Long userId, Long orderId) { public void cancelPaidOrder(Long userId, Long orderId, Integer cancelType) {
tradeOrderUpdateService.cancelPaidOrder(userId, orderId); tradeOrderUpdateService.cancelPaidOrder(userId, orderId, cancelType);
} }
} }

View File

@ -2,6 +2,7 @@ package cn.iocoder.yudao.module.trade.dal.dataobject.order;
import cn.iocoder.yudao.framework.common.enums.TerminalEnum; import cn.iocoder.yudao.framework.common.enums.TerminalEnum;
import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO; import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO;
import cn.iocoder.yudao.framework.mybatis.core.type.LongListTypeHandler;
import cn.iocoder.yudao.module.member.api.user.dto.MemberUserRespDTO; import cn.iocoder.yudao.module.member.api.user.dto.MemberUserRespDTO;
import cn.iocoder.yudao.module.trade.dal.dataobject.brokerage.BrokerageUserDO; import cn.iocoder.yudao.module.trade.dal.dataobject.brokerage.BrokerageUserDO;
import cn.iocoder.yudao.module.trade.dal.dataobject.delivery.DeliveryExpressDO; import cn.iocoder.yudao.module.trade.dal.dataobject.delivery.DeliveryExpressDO;
@ -12,10 +13,14 @@ import cn.iocoder.yudao.module.trade.enums.order.TradeOrderRefundStatusEnum;
import cn.iocoder.yudao.module.trade.enums.order.TradeOrderStatusEnum; import cn.iocoder.yudao.module.trade.enums.order.TradeOrderStatusEnum;
import cn.iocoder.yudao.module.trade.enums.order.TradeOrderTypeEnum; import cn.iocoder.yudao.module.trade.enums.order.TradeOrderTypeEnum;
import com.baomidou.mybatisplus.annotation.KeySequence; import com.baomidou.mybatisplus.annotation.KeySequence;
import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.annotation.TableName; import com.baomidou.mybatisplus.annotation.TableName;
import com.baomidou.mybatisplus.extension.handlers.JacksonTypeHandler;
import lombok.*; import lombok.*;
import java.time.LocalDateTime; import java.time.LocalDateTime;
import java.util.List;
import java.util.Map;
/** /**
* 交易订单 DO * 交易订单 DO
@ -291,6 +296,24 @@ public class TradeOrderDO extends BaseDO {
*/ */
private Integer vipPrice; private Integer vipPrice;
/**
* 赠送的优惠劵
*
* key: 优惠劵模版编号
* value对应的优惠券数量
*
* 目的用于订单支付后赠送优惠券
*/
@TableField(typeHandler = JacksonTypeHandler.class)
private Map<Long, Integer> giveCouponTemplateCounts;
/**
* 赠送的优惠劵编号
*
* 目的用于后续取消或者售后订单时需要扣减赠送
*/
@TableField(typeHandler = LongListTypeHandler.class)
private List<Long> giveCouponIds;
/** /**
* 秒杀活动编号 * 秒杀活动编号
* *

View File

@ -268,11 +268,6 @@ public class BrokerageUserServiceImpl implements BrokerageUserService {
return false; return false;
} }
// 校验分佣模式仅可后台手动设置推广员
// if (BrokerageEnabledConditionEnum.ADMIN.getCondition().equals(tradeConfig.getBrokerageEnabledCondition())) {
// throw exception(BROKERAGE_BIND_CONDITION_ADMIN);
// }
// 校验分销关系绑定模式 // 校验分销关系绑定模式
if (BrokerageBindModeEnum.REGISTER.getMode().equals(tradeConfig.getBrokerageBindMode())) { if (BrokerageBindModeEnum.REGISTER.getMode().equals(tradeConfig.getBrokerageBindMode())) {
// 判断是否为新用户注册时间在 30 秒内的都算新用户 // 判断是否为新用户注册时间在 30 秒内的都算新用户

View File

@ -1,6 +1,5 @@
package cn.iocoder.yudao.module.trade.service.order; package cn.iocoder.yudao.module.trade.service.order;
import cn.iocoder.yudao.framework.common.enums.TerminalEnum;
import cn.iocoder.yudao.module.trade.controller.admin.order.vo.TradeOrderDeliveryReqVO; import cn.iocoder.yudao.module.trade.controller.admin.order.vo.TradeOrderDeliveryReqVO;
import cn.iocoder.yudao.module.trade.controller.admin.order.vo.TradeOrderRemarkReqVO; import cn.iocoder.yudao.module.trade.controller.admin.order.vo.TradeOrderRemarkReqVO;
import cn.iocoder.yudao.module.trade.controller.admin.order.vo.TradeOrderUpdateAddressReqVO; import cn.iocoder.yudao.module.trade.controller.admin.order.vo.TradeOrderUpdateAddressReqVO;
@ -10,9 +9,10 @@ import cn.iocoder.yudao.module.trade.controller.app.order.vo.AppTradeOrderSettle
import cn.iocoder.yudao.module.trade.controller.app.order.vo.AppTradeOrderSettlementRespVO; import cn.iocoder.yudao.module.trade.controller.app.order.vo.AppTradeOrderSettlementRespVO;
import cn.iocoder.yudao.module.trade.controller.app.order.vo.item.AppTradeOrderItemCommentCreateReqVO; import cn.iocoder.yudao.module.trade.controller.app.order.vo.item.AppTradeOrderItemCommentCreateReqVO;
import cn.iocoder.yudao.module.trade.dal.dataobject.order.TradeOrderDO; import cn.iocoder.yudao.module.trade.dal.dataobject.order.TradeOrderDO;
import jakarta.validation.constraints.NotNull; import jakarta.validation.constraints.NotNull;
import java.util.List;
/** /**
* 交易订单Service 接口 * 交易订单Service 接口
* *
@ -187,13 +187,22 @@ public interface TradeOrderUpdateService {
*/ */
void updateOrderCombinationInfo(Long orderId, Long activityId, Long combinationRecordId, Long headId); void updateOrderCombinationInfo(Long orderId, Long activityId, Long combinationRecordId, Long headId);
// TODO 芋艿拼团取消不调这个接口哈
/** /**
* 取消支付订单 * 取消支付订单
* *
* @param userId 用户编号 * @param userId 用户编号
* @param orderId 订单编号 * @param orderId 订单编号
* @param cancelType 取消类型
*/ */
void cancelPaidOrder(Long userId, Long orderId); void cancelPaidOrder(Long userId, Long orderId, Integer cancelType);
/**
* 更新下单赠送的优惠券编号到订单
*
* @param userId 用户编号
* @param orderId 订单编号
* @param giveCouponIds 赠送的优惠券编号列表
*/
void updateOrderGiveCouponIds(Long userId, Long orderId, List<Long> giveCouponIds);
} }

View File

@ -18,6 +18,8 @@ import cn.iocoder.yudao.module.member.api.address.dto.MemberAddressRespDTO;
import cn.iocoder.yudao.module.pay.api.order.PayOrderApi; import cn.iocoder.yudao.module.pay.api.order.PayOrderApi;
import cn.iocoder.yudao.module.pay.api.order.dto.PayOrderCreateReqDTO; import cn.iocoder.yudao.module.pay.api.order.dto.PayOrderCreateReqDTO;
import cn.iocoder.yudao.module.pay.api.order.dto.PayOrderRespDTO; import cn.iocoder.yudao.module.pay.api.order.dto.PayOrderRespDTO;
import cn.iocoder.yudao.module.pay.api.refund.PayRefundApi;
import cn.iocoder.yudao.module.pay.api.refund.dto.PayRefundCreateReqDTO;
import cn.iocoder.yudao.module.pay.enums.order.PayOrderStatusEnum; import cn.iocoder.yudao.module.pay.enums.order.PayOrderStatusEnum;
import cn.iocoder.yudao.module.product.api.comment.ProductCommentApi; import cn.iocoder.yudao.module.product.api.comment.ProductCommentApi;
import cn.iocoder.yudao.module.product.api.comment.dto.ProductCommentCreateReqDTO; import cn.iocoder.yudao.module.product.api.comment.dto.ProductCommentCreateReqDTO;
@ -111,6 +113,8 @@ public class TradeOrderUpdateServiceImpl implements TradeOrderUpdateService {
private ProductCommentApi productCommentApi; private ProductCommentApi productCommentApi;
@Resource @Resource
public SocialClientApi socialClientApi; public SocialClientApi socialClientApi;
@Resource
public PayRefundApi payRefundApi;
@Resource @Resource
private TradeOrderProperties tradeOrderProperties; private TradeOrderProperties tradeOrderProperties;
@ -197,6 +201,8 @@ public class TradeOrderUpdateServiceImpl implements TradeOrderUpdateService {
order.setRefundStatus(TradeOrderRefundStatusEnum.NONE.getStatus()); order.setRefundStatus(TradeOrderRefundStatusEnum.NONE.getStatus());
order.setProductCount(getSumValue(calculateRespBO.getItems(), TradePriceCalculateRespBO.OrderItem::getCount, Integer::sum)); order.setProductCount(getSumValue(calculateRespBO.getItems(), TradePriceCalculateRespBO.OrderItem::getCount, Integer::sum));
order.setUserIp(getClientIP()).setTerminal(getTerminal()); order.setUserIp(getClientIP()).setTerminal(getTerminal());
// 使用 + 赠送优惠券
order.setGiveCouponTemplateCounts(calculateRespBO.getGiveCouponTemplateCounts());
// 支付 + 退款信息 // 支付 + 退款信息
order.setAdjustPrice(0).setPayStatus(false); order.setAdjustPrice(0).setPayStatus(false);
order.setRefundStatus(TradeOrderRefundStatusEnum.NONE.getStatus()).setRefundPrice(0); order.setRefundStatus(TradeOrderRefundStatusEnum.NONE.getStatus()).setRefundPrice(0);
@ -854,15 +860,46 @@ public class TradeOrderUpdateServiceImpl implements TradeOrderUpdateService {
@Override @Override
@Transactional(rollbackFor = Exception.class) @Transactional(rollbackFor = Exception.class)
public void cancelPaidOrder(Long userId, Long orderId) { public void cancelPaidOrder(Long userId, Long orderId, Integer cancelType) {
// TODO @puhui999需要校验状态已支付的情况下才可以 // 1.1 这里校验下 cancelType 只允许拼团关闭
if (ObjUtil.notEqual(TradeOrderCancelTypeEnum.COMBINATION_CLOSE.getType(), cancelType)) {
return;
}
// 1.2 检验订单存在
TradeOrderDO order = tradeOrderMapper.selectOrderByIdAndUserId(orderId, userId); TradeOrderDO order = tradeOrderMapper.selectOrderByIdAndUserId(orderId, userId);
if (order == null) { if (order == null) {
throw exception(ORDER_NOT_FOUND); throw exception(ORDER_NOT_FOUND);
} }
cancelOrder0(order, TradeOrderCancelTypeEnum.MEMBER_CANCEL);
// TODO @puhui999需要退款 // 1.3 校验订单是否支付
if (!order.getPayStatus()) {
throw exception(ORDER_CANCEL_PAID_FAIL, "已支付");
}
// 1.3 校验订单是否已退款
if (ObjUtil.equal(TradeOrderRefundStatusEnum.NONE.getStatus(), order.getRefundStatus())) {
throw exception(ORDER_CANCEL_PAID_FAIL, "未退款");
}
// 2.1 取消订单
cancelOrder0(order, TradeOrderCancelTypeEnum.COMBINATION_CLOSE);
// 2.2 创建退款单
payRefundApi.createRefund(new PayRefundCreateReqDTO()
.setAppKey(tradeOrderProperties.getPayAppKey()).setUserIp(getClientIP()) // 支付应用
.setMerchantOrderId(String.valueOf(order.getId())) // 支付单号
.setMerchantRefundId(String.valueOf(order.getId()))
.setReason(TradeOrderCancelTypeEnum.COMBINATION_CLOSE.getName()).setPrice(order.getPayPrice()));// 价格信息
}
@Override
public void updateOrderGiveCouponIds(Long userId, Long orderId, List<Long> giveCouponIds) {
// 1. 检验订单存在
TradeOrderDO order = tradeOrderMapper.selectOrderByIdAndUserId(orderId, userId);
if (order == null) {
throw exception(ORDER_NOT_FOUND);
}
// 2. 更新订单赠送的优惠券编号列表
tradeOrderMapper.updateById(new TradeOrderDO().setId(orderId).setGiveCouponIds(giveCouponIds));
} }
/** /**

View File

@ -1,12 +1,16 @@
package cn.iocoder.yudao.module.trade.service.order.handler; package cn.iocoder.yudao.module.trade.service.order.handler;
import cn.hutool.core.collection.CollUtil;
import cn.iocoder.yudao.module.promotion.api.coupon.CouponApi; import cn.iocoder.yudao.module.promotion.api.coupon.CouponApi;
import cn.iocoder.yudao.module.promotion.api.coupon.dto.CouponUseReqDTO; import cn.iocoder.yudao.module.promotion.api.coupon.dto.CouponUseReqDTO;
import cn.iocoder.yudao.module.trade.dal.dataobject.order.TradeOrderDO; import cn.iocoder.yudao.module.trade.dal.dataobject.order.TradeOrderDO;
import cn.iocoder.yudao.module.trade.dal.dataobject.order.TradeOrderItemDO; import cn.iocoder.yudao.module.trade.dal.dataobject.order.TradeOrderItemDO;
import cn.iocoder.yudao.module.trade.service.order.TradeOrderQueryService;
import cn.iocoder.yudao.module.trade.service.order.TradeOrderUpdateService;
import jakarta.annotation.Resource;
import org.springframework.context.annotation.Lazy;
import org.springframework.stereotype.Component; import org.springframework.stereotype.Component;
import jakarta.annotation.Resource;
import java.util.List; import java.util.List;
/** /**
@ -17,6 +21,12 @@ import java.util.List;
@Component @Component
public class TradeCouponOrderHandler implements TradeOrderHandler { public class TradeCouponOrderHandler implements TradeOrderHandler {
@Resource
@Lazy // 延迟加载避免循环依赖
private TradeOrderUpdateService orderUpdateService;
@Resource
private TradeOrderQueryService orderQueryService;
@Resource @Resource
private CouponApi couponApi; private CouponApi couponApi;
@ -31,12 +41,30 @@ public class TradeCouponOrderHandler implements TradeOrderHandler {
} }
@Override @Override
public void afterCancelOrder(TradeOrderDO order, List<TradeOrderItemDO> orderItems) { public void afterPayOrder(TradeOrderDO order, List<TradeOrderItemDO> orderItems) {
if (order.getCouponId() == null || order.getCouponId() <= 0) { if (CollUtil.isEmpty(order.getGiveCouponTemplateCounts())) {
return; return;
} }
// 退回优惠劵 // 赠送优惠券
couponApi.returnUsedCoupon(order.getCouponId()); List<Long> couponIds = couponApi.takeCouponsByAdmin(order.getGiveCouponTemplateCounts(), order.getUserId());
if (CollUtil.isEmpty(couponIds)) {
return;
}
orderUpdateService.updateOrderGiveCouponIds(order.getUserId(), order.getId(), couponIds);
}
@Override
public void afterCancelOrder(TradeOrderDO order, List<TradeOrderItemDO> orderItems) {
// 情况一退还订单使用的优惠券
if (order.getCouponId() != null && order.getCouponId() > 0) {
// 退回优惠劵
couponApi.returnUsedCoupon(order.getCouponId());
}
// 情况二收回赠送的优惠券
if (CollUtil.isEmpty(order.getGiveCouponIds())) {
return;
}
couponApi.invalidateCouponsByAdmin(order.getGiveCouponIds(), order.getUserId());
} }
} }

View File

@ -6,6 +6,7 @@ import cn.iocoder.yudao.module.trade.enums.order.TradeOrderTypeEnum;
import lombok.Data; import lombok.Data;
import java.util.List; import java.util.List;
import java.util.Map;
/** /**
* 价格计算 Response BO * 价格计算 Response BO
@ -67,6 +68,21 @@ public class TradePriceCalculateRespBO {
*/ */
private Long bargainActivityId; private Long bargainActivityId;
/**
* 是否包邮
*/
private Boolean freeDelivery;
/**
* 赠送的优惠劵
*
* key: 优惠劵模版编号
* value对应的优惠券数量
*
* 目的用于订单支付后赠送优惠券
*/
private Map<Long, Integer> giveCouponTemplateCounts;
/** /**
* 订单价格 * 订单价格
*/ */
@ -213,8 +229,19 @@ public class TradePriceCalculateRespBO {
*/ */
private Long categoryId; private Long categoryId;
// ========== 物流相关字段 =========
/** /**
* 运费模板 Id * 配送方式数组
*
* 对应 DeliveryTypeEnum 枚举
*/
private List<Integer> deliveryTypes;
/**
* 物流配置模板编号
*
* 对应 TradeDeliveryExpressTemplateDO id 编号
*/ */
private Long deliveryTemplateId; private Long deliveryTemplateId;
@ -234,7 +261,7 @@ public class TradePriceCalculateRespBO {
private List<ProductPropertyValueDetailRespDTO> properties; private List<ProductPropertyValueDetailRespDTO> properties;
/** /**
* 使用的积分 * 赠送的积分
*/ */
private Integer givePoint; private Integer givePoint;

View File

@ -25,6 +25,7 @@ import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.
import static cn.iocoder.yudao.module.promotion.enums.ErrorCodeConstants.COUPON_NO_MATCH_MIN_PRICE; import static cn.iocoder.yudao.module.promotion.enums.ErrorCodeConstants.COUPON_NO_MATCH_MIN_PRICE;
import static cn.iocoder.yudao.module.promotion.enums.ErrorCodeConstants.COUPON_NO_MATCH_SPU; import static cn.iocoder.yudao.module.promotion.enums.ErrorCodeConstants.COUPON_NO_MATCH_SPU;
import static cn.iocoder.yudao.module.trade.enums.ErrorCodeConstants.PRICE_CALCULATE_COUPON_NOT_MATCH_NORMAL_ORDER; import static cn.iocoder.yudao.module.trade.enums.ErrorCodeConstants.PRICE_CALCULATE_COUPON_NOT_MATCH_NORMAL_ORDER;
import static cn.iocoder.yudao.module.trade.enums.ErrorCodeConstants.PRICE_CALCULATE_COUPON_PRICE_TOO_MUCH;
/** /**
* 优惠劵的 {@link TradePriceCalculator} 实现类 * 优惠劵的 {@link TradePriceCalculator} 实现类
@ -65,8 +66,9 @@ public class TradeCouponPriceCalculator implements TradePriceCalculator {
// 3.1 计算可以优惠的金额 // 3.1 计算可以优惠的金额
Integer couponPrice = getCouponPrice(coupon, totalPayPrice); Integer couponPrice = getCouponPrice(coupon, totalPayPrice);
Assert.isTrue(couponPrice < totalPayPrice, if (couponPrice <= totalPayPrice) {
"优惠劵({}) 的优惠金额({}),不能大于订单总金额({})", coupon.getId(), couponPrice, totalPayPrice); throw exception(PRICE_CALCULATE_COUPON_PRICE_TOO_MUCH);
}
// 3.2 计算分摊的优惠金额 // 3.2 计算分摊的优惠金额
List<Integer> divideCouponPrices = TradePriceCalculatorHelper.dividePrice(orderItems, couponPrice); List<Integer> divideCouponPrices = TradePriceCalculatorHelper.dividePrice(orderItems, couponPrice);

View File

@ -4,6 +4,7 @@ import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.lang.Assert; import cn.hutool.core.lang.Assert;
import cn.hutool.core.util.StrUtil; import cn.hutool.core.util.StrUtil;
import cn.iocoder.yudao.framework.common.enums.CommonStatusEnum; import cn.iocoder.yudao.framework.common.enums.CommonStatusEnum;
import cn.iocoder.yudao.framework.common.util.collection.CollectionUtils;
import cn.iocoder.yudao.module.member.api.address.MemberAddressApi; import cn.iocoder.yudao.module.member.api.address.MemberAddressApi;
import cn.iocoder.yudao.module.member.api.address.dto.MemberAddressRespDTO; import cn.iocoder.yudao.module.member.api.address.dto.MemberAddressRespDTO;
import cn.iocoder.yudao.module.trade.dal.dataobject.config.TradeConfigDO; import cn.iocoder.yudao.module.trade.dal.dataobject.config.TradeConfigDO;
@ -17,11 +18,11 @@ import cn.iocoder.yudao.module.trade.service.delivery.bo.DeliveryExpressTemplate
import cn.iocoder.yudao.module.trade.service.price.bo.TradePriceCalculateReqBO; import cn.iocoder.yudao.module.trade.service.price.bo.TradePriceCalculateReqBO;
import cn.iocoder.yudao.module.trade.service.price.bo.TradePriceCalculateRespBO; import cn.iocoder.yudao.module.trade.service.price.bo.TradePriceCalculateRespBO;
import cn.iocoder.yudao.module.trade.service.price.bo.TradePriceCalculateRespBO.OrderItem; import cn.iocoder.yudao.module.trade.service.price.bo.TradePriceCalculateRespBO.OrderItem;
import jakarta.annotation.Resource;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.springframework.core.annotation.Order; import org.springframework.core.annotation.Order;
import org.springframework.stereotype.Component; import org.springframework.stereotype.Component;
import jakarta.annotation.Resource;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.Set; import java.util.Set;
@ -55,7 +56,11 @@ public class TradeDeliveryPriceCalculator implements TradePriceCalculator {
if (param.getDeliveryType() == null) { if (param.getDeliveryType() == null) {
return; return;
} }
// TODO @puhui999需要校验是不是存在商品不能门店自提或者不能快递发货的情况就是说配送方式不匹配哈 // 校验是不是存在商品不能门店自提或者不能快递发货的情况就是说配送方式不匹配哈
if (CollectionUtils.anyMatch(result.getItems(), item -> !item.getDeliveryTypes().contains(param.getDeliveryType()))) {
throw exception(PRICE_CALCULATE_DELIVERY_PRICE_TYPE_ILLEGAL);
}
if (DeliveryTypeEnum.PICK_UP.getType().equals(param.getDeliveryType())) { if (DeliveryTypeEnum.PICK_UP.getType().equals(param.getDeliveryType())) {
calculateByPickUp(param); calculateByPickUp(param);
} else if (DeliveryTypeEnum.EXPRESS.getType().equals(param.getDeliveryType())) { } else if (DeliveryTypeEnum.EXPRESS.getType().equals(param.getDeliveryType())) {
@ -90,7 +95,12 @@ public class TradeDeliveryPriceCalculator implements TradePriceCalculator {
return; return;
} }
// 情况二快递模版 // 情况二活动包邮
if (Boolean.TRUE.equals(result.getFreeDelivery())) {
return;
}
// 情况三快递模版
// 2.1 过滤出已选中的商品 SKU // 2.1 过滤出已选中的商品 SKU
List<OrderItem> selectedItem = filterList(result.getItems(), OrderItem::getSelected); List<OrderItem> selectedItem = filterList(result.getItems(), OrderItem::getSelected);
Set<Long> deliveryTemplateIds = convertSet(selectedItem, OrderItem::getDeliveryTemplateId); Set<Long> deliveryTemplateIds = convertSet(selectedItem, OrderItem::getDeliveryTemplateId);
@ -124,7 +134,7 @@ public class TradeDeliveryPriceCalculator implements TradePriceCalculator {
Map<Long, List<OrderItem>> template2ItemMap = convertMultiMap(selectedSkus, OrderItem::getDeliveryTemplateId); Map<Long, List<OrderItem>> template2ItemMap = convertMultiMap(selectedSkus, OrderItem::getDeliveryTemplateId);
// 依次计算快递运费 // 依次计算快递运费
for (Map.Entry<Long, List<OrderItem>> entry : template2ItemMap.entrySet()) { for (Map.Entry<Long, List<OrderItem>> entry : template2ItemMap.entrySet()) {
Long templateId = entry.getKey(); Long templateId = entry.getKey();
List<OrderItem> orderItems = entry.getValue(); List<OrderItem> orderItems = entry.getValue();
DeliveryExpressTemplateRespBO templateBO = expressTemplateMap.get(templateId); DeliveryExpressTemplateRespBO templateBO = expressTemplateMap.get(templateId);
if (templateBO == null) { if (templateBO == null) {
@ -144,8 +154,8 @@ public class TradeDeliveryPriceCalculator implements TradePriceCalculator {
/** /**
* 按配送方式来计算运费 * 按配送方式来计算运费
* *
* @param orderItems SKU 商品项目 * @param orderItems SKU 商品项目
* @param chargeMode 配送计费方式 * @param chargeMode 配送计费方式
* @param templateCharge 快递运费配置 * @param templateCharge 快递运费配置
*/ */
private void calculateExpressFeeByChargeMode(List<OrderItem> orderItems, Integer chargeMode, private void calculateExpressFeeByChargeMode(List<OrderItem> orderItems, Integer chargeMode,

View File

@ -11,6 +11,7 @@ import cn.iocoder.yudao.module.trade.service.price.bo.TradePriceCalculateReqBO;
import cn.iocoder.yudao.module.trade.service.price.bo.TradePriceCalculateRespBO; import cn.iocoder.yudao.module.trade.service.price.bo.TradePriceCalculateRespBO;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.LinkedHashMap;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
@ -31,8 +32,7 @@ public class TradePriceCalculatorHelper {
List<ProductSpuRespDTO> spuList, List<ProductSkuRespDTO> skuList) { List<ProductSpuRespDTO> spuList, List<ProductSkuRespDTO> skuList) {
// 创建 PriceCalculateRespDTO 对象 // 创建 PriceCalculateRespDTO 对象
TradePriceCalculateRespBO result = new TradePriceCalculateRespBO(); TradePriceCalculateRespBO result = new TradePriceCalculateRespBO();
result.setType(getOrderType(param)); result.setType(getOrderType(param)).setPromotions(new ArrayList<>()).setGiveCouponTemplateCounts(new LinkedHashMap<>());
result.setPromotions(new ArrayList<>());
// 创建它的 OrderItem 属性 // 创建它的 OrderItem 属性
result.setItems(new ArrayList<>(param.getItems().size())); result.setItems(new ArrayList<>(param.getItems().size()));
@ -60,7 +60,7 @@ public class TradePriceCalculatorHelper {
.setWeight(sku.getWeight()).setVolume(sku.getVolume()); .setWeight(sku.getWeight()).setVolume(sku.getVolume());
// spu 信息 // spu 信息
orderItem.setSpuName(spu.getName()).setCategoryId(spu.getCategoryId()) orderItem.setSpuName(spu.getName()).setCategoryId(spu.getCategoryId())
.setDeliveryTemplateId(spu.getDeliveryTemplateId()) .setDeliveryTypes(spu.getDeliveryTypes()).setDeliveryTemplateId(spu.getDeliveryTemplateId())
.setGivePoint(spu.getGiveIntegral()).setUsePoint(0); .setGivePoint(spu.getGiveIntegral()).setUsePoint(0);
if (StrUtil.isBlank(orderItem.getPicUrl())) { if (StrUtil.isBlank(orderItem.getPicUrl())) {
orderItem.setPicUrl(spu.getPicUrl()); orderItem.setPicUrl(spu.getPicUrl());

View File

@ -3,23 +3,30 @@ package cn.iocoder.yudao.module.trade.service.price.calculator;
import cn.hutool.core.collection.CollUtil; import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.util.ObjectUtil; import cn.hutool.core.util.ObjectUtil;
import cn.hutool.core.util.StrUtil; import cn.hutool.core.util.StrUtil;
import cn.iocoder.yudao.framework.common.util.number.MoneyUtils;
import cn.iocoder.yudao.module.promotion.api.reward.RewardActivityApi; import cn.iocoder.yudao.module.promotion.api.reward.RewardActivityApi;
import cn.iocoder.yudao.module.promotion.api.reward.dto.RewardActivityMatchRespDTO; import cn.iocoder.yudao.module.promotion.api.reward.dto.RewardActivityMatchRespDTO;
import cn.iocoder.yudao.module.promotion.enums.common.PromotionConditionTypeEnum; import cn.iocoder.yudao.module.promotion.enums.common.PromotionConditionTypeEnum;
import cn.iocoder.yudao.module.promotion.enums.common.PromotionProductScopeEnum;
import cn.iocoder.yudao.module.promotion.enums.common.PromotionTypeEnum; import cn.iocoder.yudao.module.promotion.enums.common.PromotionTypeEnum;
import cn.iocoder.yudao.module.trade.enums.order.TradeOrderTypeEnum; import cn.iocoder.yudao.module.trade.enums.order.TradeOrderTypeEnum;
import cn.iocoder.yudao.module.trade.service.price.bo.TradePriceCalculateReqBO; import cn.iocoder.yudao.module.trade.service.price.bo.TradePriceCalculateReqBO;
import cn.iocoder.yudao.module.trade.service.price.bo.TradePriceCalculateRespBO; import cn.iocoder.yudao.module.trade.service.price.bo.TradePriceCalculateRespBO;
import jakarta.annotation.Resource;
import org.springframework.core.annotation.Order; import org.springframework.core.annotation.Order;
import org.springframework.stereotype.Component; import org.springframework.stereotype.Component;
import jakarta.annotation.Resource; import java.util.ArrayList;
import java.util.Comparator;
import java.util.List; import java.util.List;
import java.util.Map;
import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertSet; import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertSet;
import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.filterList; import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.filterList;
import static cn.iocoder.yudao.module.trade.service.price.calculator.TradePriceCalculatorHelper.formatPrice; import static cn.iocoder.yudao.module.trade.service.price.calculator.TradePriceCalculatorHelper.formatPrice;
// TODO @puhui999相关的单测建议改一改
/** /**
* 满减送活动的 {@link TradePriceCalculator} 实现类 * 满减送活动的 {@link TradePriceCalculator} 实现类
* *
@ -52,7 +59,7 @@ public class TradeRewardActivityPriceCalculator implements TradePriceCalculator
private void calculate(TradePriceCalculateReqBO param, TradePriceCalculateRespBO result, private void calculate(TradePriceCalculateReqBO param, TradePriceCalculateRespBO result,
RewardActivityMatchRespDTO rewardActivity) { RewardActivityMatchRespDTO rewardActivity) {
// 1.1 获得满减送的订单项商品列表 // 1.1 获得满减送的订单项商品列表
List<TradePriceCalculateRespBO.OrderItem> orderItems = filterMatchCouponOrderItems(result, rewardActivity); List<TradePriceCalculateRespBO.OrderItem> orderItems = filterMatchActivityOrderItems(result, rewardActivity);
if (CollUtil.isEmpty(orderItems)) { if (CollUtil.isEmpty(orderItems)) {
return; return;
} }
@ -61,7 +68,7 @@ public class TradeRewardActivityPriceCalculator implements TradePriceCalculator
if (rule == null) { if (rule == null) {
TradePriceCalculatorHelper.addNotMatchPromotion(result, orderItems, TradePriceCalculatorHelper.addNotMatchPromotion(result, orderItems,
rewardActivity.getId(), rewardActivity.getName(), PromotionTypeEnum.REWARD_ACTIVITY.getType(), rewardActivity.getId(), rewardActivity.getName(), PromotionTypeEnum.REWARD_ACTIVITY.getType(),
getRewardActivityNotMeetTip(rewardActivity)); getRewardActivityNotMeetTip(rewardActivity, orderItems));
return; return;
} }
@ -84,6 +91,36 @@ public class TradeRewardActivityPriceCalculator implements TradePriceCalculator
TradePriceCalculatorHelper.recountPayPrice(orderItem); TradePriceCalculatorHelper.recountPayPrice(orderItem);
} }
TradePriceCalculatorHelper.recountAllPrice(result); TradePriceCalculatorHelper.recountAllPrice(result);
// 4.1 记录赠送的积分
if (rule.getPoint() != null && rule.getPoint() > 0) {
List<Integer> dividePoints = TradePriceCalculatorHelper.dividePrice(orderItems, rule.getPoint());
for (int i = 0; i < orderItems.size(); i++) {
// 商品可能赠送了积分所以这里要加上
TradePriceCalculateRespBO.OrderItem orderItem = orderItems.get(i);
orderItem.setGivePoint(orderItem.getGivePoint() + dividePoints.get(i));
}
}
// 4.2 记录订单是否包邮
if (Boolean.TRUE.equals(rule.getFreeDelivery())) {
// 只要满足一个活动包邮那么这单就包邮
result.setFreeDelivery(true);
}
// 4.3 记录赠送的优惠券
if (CollUtil.isNotEmpty(rule.getGiveCouponTemplateCounts())) {
for (Map.Entry<Long, Integer> entry : rule.getGiveCouponTemplateCounts().entrySet()) {
Map<Long, Integer> giveCouponTemplateCounts = result.getGiveCouponTemplateCounts();
// TODO @puhui999是不是有一种可能性这个 key 没有别的 key 有哈
// TODO 这里还有一种简化的写法就是下面大概两行就可以啦
// result.getGiveCouponTemplateCounts().put(entry.getKey(),
// result.getGiveCouponTemplateCounts().getOrDefault(entry.getKey(), 0) + entry.getValue());
if (giveCouponTemplateCounts.get(entry.getKey()) == null) { // 情况一还没有赠送的优惠券
result.setGiveCouponTemplateCounts(rule.getGiveCouponTemplateCounts());
} else { // 情况二别的满减活动送过同类优惠券则直接增加数量
giveCouponTemplateCounts.put(entry.getKey(), giveCouponTemplateCounts.get(entry.getKey()) + entry.getValue());
}
}
}
} }
/** /**
@ -93,10 +130,23 @@ public class TradeRewardActivityPriceCalculator implements TradePriceCalculator
* @param rewardActivity 满减送活动 * @param rewardActivity 满减送活动
* @return 订单项商品列表 * @return 订单项商品列表
*/ */
private List<TradePriceCalculateRespBO.OrderItem> filterMatchCouponOrderItems(TradePriceCalculateRespBO result, private List<TradePriceCalculateRespBO.OrderItem> filterMatchActivityOrderItems(TradePriceCalculateRespBO result,
RewardActivityMatchRespDTO rewardActivity) { RewardActivityMatchRespDTO rewardActivity) {
return filterList(result.getItems(), // 情况一全部商品都可以参与
orderItem -> CollUtil.contains(rewardActivity.getSpuIds(), orderItem.getSpuId())); if (PromotionProductScopeEnum.isAll(rewardActivity.getProductScope())) {
return result.getItems();
}
// 情况二指定商品参与
if (PromotionProductScopeEnum.isSpu(rewardActivity.getProductScope())) {
return filterList(result.getItems(),
orderItem -> CollUtil.contains(rewardActivity.getProductScopeValues(), orderItem.getSpuId()));
}
// 情况三指定商品类型参与
if (PromotionProductScopeEnum.isCategory(rewardActivity.getProductScope())) {
return filterList(result.getItems(),
orderItem -> CollUtil.contains(rewardActivity.getProductScopeValues(), orderItem.getCategoryId()));
}
return List.of();
} }
/** /**
@ -129,14 +179,30 @@ public class TradeRewardActivityPriceCalculator implements TradePriceCalculator
} }
/** /**
* 获得满减送活动匹配时的提示 * 获得满减送活动匹配时的提示
* *
* @param rewardActivity 满减送活动 * @param rewardActivity 满减送活动
* @return 提示 * @return 提示
*/ */
private String getRewardActivityNotMeetTip(RewardActivityMatchRespDTO rewardActivity) { private String getRewardActivityNotMeetTip(RewardActivityMatchRespDTO rewardActivity,
// TODO 芋艿后面再想想应该找第一个规则算下还差多少即可 List<TradePriceCalculateRespBO.OrderItem> orderItems) {
return "TODO"; // 1. 计算数量和价格
Integer count = TradePriceCalculatorHelper.calculateTotalCount(orderItems);
Integer price = TradePriceCalculatorHelper.calculateTotalPayPrice(orderItems);
assert count != null && price != null;
// 2. 构建不满足时的提示信息按最低档规则算
String meetTip = "满减送:购满 {} {},可以减 {} 元";
List<RewardActivityMatchRespDTO.Rule> rules = new ArrayList<>(rewardActivity.getRules());
rules.sort(Comparator.comparing(RewardActivityMatchRespDTO.Rule::getLimit)); // 按优惠门槛升序
RewardActivityMatchRespDTO.Rule rule = rules.get(0);
if (PromotionConditionTypeEnum.PRICE.getType().equals(rewardActivity.getConditionType())) {
return StrUtil.format(meetTip, rule.getLimit(), "", MoneyUtils.fenToYuanStr(rule.getDiscountPrice()));
}
if (PromotionConditionTypeEnum.COUNT.getType().equals(rewardActivity.getConditionType())) {
return StrUtil.format(meetTip, rule.getLimit(), "", MoneyUtils.fenToYuanStr(rule.getDiscountPrice()));
}
return StrUtil.EMPTY;
} }
} }

View File

@ -4,6 +4,7 @@ import cn.iocoder.yudao.framework.test.core.ut.BaseMockitoUnitTest;
import cn.iocoder.yudao.module.promotion.api.reward.RewardActivityApi; import cn.iocoder.yudao.module.promotion.api.reward.RewardActivityApi;
import cn.iocoder.yudao.module.promotion.api.reward.dto.RewardActivityMatchRespDTO; import cn.iocoder.yudao.module.promotion.api.reward.dto.RewardActivityMatchRespDTO;
import cn.iocoder.yudao.module.promotion.enums.common.PromotionConditionTypeEnum; import cn.iocoder.yudao.module.promotion.enums.common.PromotionConditionTypeEnum;
import cn.iocoder.yudao.module.promotion.enums.common.PromotionProductScopeEnum;
import cn.iocoder.yudao.module.promotion.enums.common.PromotionTypeEnum; import cn.iocoder.yudao.module.promotion.enums.common.PromotionTypeEnum;
import cn.iocoder.yudao.module.trade.enums.order.TradeOrderTypeEnum; import cn.iocoder.yudao.module.trade.enums.order.TradeOrderTypeEnum;
import cn.iocoder.yudao.module.trade.service.price.bo.TradePriceCalculateReqBO; import cn.iocoder.yudao.module.trade.service.price.bo.TradePriceCalculateReqBO;
@ -13,6 +14,7 @@ import org.mockito.InjectMocks;
import org.mockito.Mock; import org.mockito.Mock;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.LinkedHashMap;
import static cn.iocoder.yudao.framework.common.util.collection.SetUtils.asSet; import static cn.iocoder.yudao.framework.common.util.collection.SetUtils.asSet;
import static cn.iocoder.yudao.framework.test.core.util.RandomUtils.randomPojo; import static cn.iocoder.yudao.framework.test.core.util.RandomUtils.randomPojo;
@ -47,7 +49,7 @@ public class TradeRewardActivityPriceCalculatorTest extends BaseMockitoUnitTest
TradePriceCalculateRespBO result = new TradePriceCalculateRespBO() TradePriceCalculateRespBO result = new TradePriceCalculateRespBO()
.setType(TradeOrderTypeEnum.NORMAL.getType()) .setType(TradeOrderTypeEnum.NORMAL.getType())
.setPrice(new TradePriceCalculateRespBO.Price()) .setPrice(new TradePriceCalculateRespBO.Price())
.setPromotions(new ArrayList<>()) .setPromotions(new ArrayList<>()).setGiveCouponTemplateCounts(new LinkedHashMap<>())
.setItems(asList( .setItems(asList(
new TradePriceCalculateRespBO.OrderItem().setSkuId(10L).setCount(2).setSelected(true) new TradePriceCalculateRespBO.OrderItem().setSkuId(10L).setCount(2).setSelected(true)
.setPrice(100).setSpuId(1L), .setPrice(100).setSpuId(1L),
@ -60,16 +62,22 @@ public class TradeRewardActivityPriceCalculatorTest extends BaseMockitoUnitTest
TradePriceCalculatorHelper.recountPayPrice(result.getItems()); TradePriceCalculatorHelper.recountPayPrice(result.getItems());
TradePriceCalculatorHelper.recountAllPrice(result); TradePriceCalculatorHelper.recountAllPrice(result);
// mock 方法限时折扣 DiscountActivity 信息 // mock 方法满减送 RewardActivity 信息
when(rewardActivityApi.getMatchRewardActivityList(eq(asSet(1L, 2L, 3L)))).thenReturn(asList( when(rewardActivityApi.getMatchRewardActivityList(eq(asSet(1L, 2L, 3L)))).thenReturn(asList(
randomPojo(RewardActivityMatchRespDTO.class, o -> o.setId(1000L).setName("活动 1000 号") randomPojo(RewardActivityMatchRespDTO.class, o -> o.setId(1000L).setName("活动 1000 号")
.setSpuIds(asList(1L, 2L)).setConditionType(PromotionConditionTypeEnum.PRICE.getType()) .setConditionType(PromotionConditionTypeEnum.PRICE.getType())
.setRules(singletonList(new RewardActivityMatchRespDTO.Rule().setLimit(200).setDiscountPrice(70)))), .setProductScope(PromotionProductScopeEnum.SPU.getScope()).setProductScopeValues(asList(1L, 2L))
.setRules(singletonList(new RewardActivityMatchRespDTO.Rule().setLimit(20).setDiscountPrice(70)
.setFreeDelivery(false)))),
randomPojo(RewardActivityMatchRespDTO.class, o -> o.setId(2000L).setName("活动 2000 号") randomPojo(RewardActivityMatchRespDTO.class, o -> o.setId(2000L).setName("活动 2000 号")
.setSpuIds(singletonList(3L)).setConditionType(PromotionConditionTypeEnum.COUNT.getType()) .setConditionType(PromotionConditionTypeEnum.COUNT.getType())
.setRules(asList(new RewardActivityMatchRespDTO.Rule().setLimit(1).setDiscountPrice(10), .setProductScope(PromotionProductScopeEnum.SPU.getScope()).setProductScopeValues(singletonList(3L))
new RewardActivityMatchRespDTO.Rule().setLimit(2).setDiscountPrice(60), // 最大可满足因为是 4 .setRules(asList(new RewardActivityMatchRespDTO.Rule().setLimit(1).setDiscountPrice(10)
new RewardActivityMatchRespDTO.Rule().setLimit(10).setDiscountPrice(100)))) .setPoint(50).setFreeDelivery(false),
new RewardActivityMatchRespDTO.Rule().setLimit(2).setDiscountPrice(60)
.setPoint(100).setFreeDelivery(false), // 最大可满足因为是 4
new RewardActivityMatchRespDTO.Rule().setLimit(10).setDiscountPrice(100)
.setFreeDelivery(false))))
)); ));
// 调用 // 调用
@ -94,6 +102,7 @@ public class TradeRewardActivityPriceCalculatorTest extends BaseMockitoUnitTest
assertEquals(orderItem01.getCouponPrice(), 0); assertEquals(orderItem01.getCouponPrice(), 0);
assertEquals(orderItem01.getPointPrice(), 0); assertEquals(orderItem01.getPointPrice(), 0);
assertEquals(orderItem01.getPayPrice(), 160); assertEquals(orderItem01.getPayPrice(), 160);
assertEquals(orderItem01.getGivePoint(), 0);
// 断言SKU 2 // 断言SKU 2
TradePriceCalculateRespBO.OrderItem orderItem02 = result.getItems().get(1); TradePriceCalculateRespBO.OrderItem orderItem02 = result.getItems().get(1);
assertEquals(orderItem02.getSkuId(), 20L); assertEquals(orderItem02.getSkuId(), 20L);
@ -104,6 +113,7 @@ public class TradeRewardActivityPriceCalculatorTest extends BaseMockitoUnitTest
assertEquals(orderItem02.getCouponPrice(), 0); assertEquals(orderItem02.getCouponPrice(), 0);
assertEquals(orderItem02.getPointPrice(), 0); assertEquals(orderItem02.getPointPrice(), 0);
assertEquals(orderItem02.getPayPrice(), 120); assertEquals(orderItem02.getPayPrice(), 120);
assertEquals(orderItem02.getGivePoint(), 0);
// 断言SKU 3 // 断言SKU 3
TradePriceCalculateRespBO.OrderItem orderItem03 = result.getItems().get(2); TradePriceCalculateRespBO.OrderItem orderItem03 = result.getItems().get(2);
assertEquals(orderItem03.getSkuId(), 30L); assertEquals(orderItem03.getSkuId(), 30L);
@ -114,6 +124,7 @@ public class TradeRewardActivityPriceCalculatorTest extends BaseMockitoUnitTest
assertEquals(orderItem03.getCouponPrice(), 0); assertEquals(orderItem03.getCouponPrice(), 0);
assertEquals(orderItem03.getPointPrice(), 0); assertEquals(orderItem03.getPointPrice(), 0);
assertEquals(orderItem03.getPayPrice(), 60); assertEquals(orderItem03.getPayPrice(), 60);
assertEquals(orderItem03.getGivePoint(), 100);
// 断言Promotion 部分第一个 // 断言Promotion 部分第一个
assertEquals(result.getPromotions().size(), 2); assertEquals(result.getPromotions().size(), 2);
TradePriceCalculateRespBO.Promotion promotion01 = result.getPromotions().get(0); TradePriceCalculateRespBO.Promotion promotion01 = result.getPromotions().get(0);
@ -175,7 +186,7 @@ public class TradeRewardActivityPriceCalculatorTest extends BaseMockitoUnitTest
// mock 方法限时折扣 DiscountActivity 信息 // mock 方法限时折扣 DiscountActivity 信息
when(rewardActivityApi.getMatchRewardActivityList(eq(asSet(1L, 2L)))).thenReturn(singletonList( when(rewardActivityApi.getMatchRewardActivityList(eq(asSet(1L, 2L)))).thenReturn(singletonList(
randomPojo(RewardActivityMatchRespDTO.class, o -> o.setId(1000L).setName("活动 1000 号") randomPojo(RewardActivityMatchRespDTO.class, o -> o.setId(1000L).setName("活动 1000 号")
.setSpuIds(asList(1L, 2L)).setConditionType(PromotionConditionTypeEnum.PRICE.getType()) .setProductScopeValues(asList(1L, 2L)).setConditionType(PromotionConditionTypeEnum.PRICE.getType())
.setRules(singletonList(new RewardActivityMatchRespDTO.Rule().setLimit(351).setDiscountPrice(70)))) .setRules(singletonList(new RewardActivityMatchRespDTO.Rule().setLimit(351).setDiscountPrice(70))))
)); ));

View File

@ -48,6 +48,7 @@ CREATE TABLE IF NOT EXISTS "trade_order"
"give_point" int NULL, "give_point" int NULL,
"refund_point" int NULL, "refund_point" int NULL,
"vip_price" int NULL, "vip_price" int NULL,
"give_coupons_map" varchar NULL,
"seckill_activity_id" long NULL, "seckill_activity_id" long NULL,
"bargain_activity_id" long NULL, "bargain_activity_id" long NULL,
"bargain_record_id" long NULL, "bargain_record_id" long NULL,

View File

@ -18,12 +18,12 @@ import cn.iocoder.yudao.module.member.service.user.MemberUserService;
import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter; import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.tags.Tag; import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.annotation.Resource;
import jakarta.validation.Valid;
import org.springframework.security.access.prepost.PreAuthorize; import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.validation.annotation.Validated; import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.*; import org.springframework.web.bind.annotation.*;
import jakarta.annotation.Resource;
import jakarta.validation.Valid;
import java.util.Collection; import java.util.Collection;
import java.util.List; import java.util.List;
import java.util.Objects; import java.util.Objects;
@ -76,14 +76,6 @@ public class MemberUserController {
return success(true); return success(true);
} }
@PutMapping("/update-balance")
@Operation(summary = "更新会员用户余额")
@PreAuthorize("@ss.hasPermission('member:user:update-balance')")
public CommonResult<Boolean> updateUserBalance(@Valid @RequestBody Long id) {
// todo @jason增加一个修改余额
return success(true);
}
@GetMapping("/get") @GetMapping("/get")
@Operation(summary = "获得会员用户") @Operation(summary = "获得会员用户")
@Parameter(name = "id", description = "编号", required = true, example = "1024") @Parameter(name = "id", description = "编号", required = true, example = "1024")

View File

@ -18,9 +18,8 @@ public enum PayWalletBizTypeEnum implements IntArrayValuable {
RECHARGE(1, "充值"), RECHARGE(1, "充值"),
RECHARGE_REFUND(2, "充值退款"), RECHARGE_REFUND(2, "充值退款"),
PAYMENT(3, "支付"), PAYMENT(3, "支付"),
PAYMENT_REFUND(4, "支付退款"); PAYMENT_REFUND(4, "支付退款"),
UPDATE_BALANCE(5, "更新余额");
// TODO 后续增加
/** /**
* 业务分类 * 业务分类

View File

@ -4,9 +4,11 @@ import cn.iocoder.yudao.framework.common.pojo.CommonResult;
import cn.iocoder.yudao.framework.common.pojo.PageResult; import cn.iocoder.yudao.framework.common.pojo.PageResult;
import cn.iocoder.yudao.module.pay.controller.admin.wallet.vo.wallet.PayWalletPageReqVO; import cn.iocoder.yudao.module.pay.controller.admin.wallet.vo.wallet.PayWalletPageReqVO;
import cn.iocoder.yudao.module.pay.controller.admin.wallet.vo.wallet.PayWalletRespVO; import cn.iocoder.yudao.module.pay.controller.admin.wallet.vo.wallet.PayWalletRespVO;
import cn.iocoder.yudao.module.pay.controller.admin.wallet.vo.wallet.PayWalletUpdateBalanceReqVO;
import cn.iocoder.yudao.module.pay.controller.admin.wallet.vo.wallet.PayWalletUserReqVO; import cn.iocoder.yudao.module.pay.controller.admin.wallet.vo.wallet.PayWalletUserReqVO;
import cn.iocoder.yudao.module.pay.convert.wallet.PayWalletConvert; import cn.iocoder.yudao.module.pay.convert.wallet.PayWalletConvert;
import cn.iocoder.yudao.module.pay.dal.dataobject.wallet.PayWalletDO; import cn.iocoder.yudao.module.pay.dal.dataobject.wallet.PayWalletDO;
import cn.iocoder.yudao.module.pay.enums.wallet.PayWalletBizTypeEnum;
import cn.iocoder.yudao.module.pay.service.wallet.PayWalletService; import cn.iocoder.yudao.module.pay.service.wallet.PayWalletService;
import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag; import io.swagger.v3.oas.annotations.tags.Tag;
@ -15,12 +17,12 @@ import jakarta.validation.Valid;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.springframework.security.access.prepost.PreAuthorize; import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.validation.annotation.Validated; import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.*;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import static cn.iocoder.yudao.framework.common.enums.UserTypeEnum.MEMBER; import static cn.iocoder.yudao.framework.common.enums.UserTypeEnum.MEMBER;
import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception;
import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success; import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success;
import static cn.iocoder.yudao.module.pay.enums.ErrorCodeConstants.WALLET_NOT_FOUND;
@Tag(name = "管理后台 - 用户钱包") @Tag(name = "管理后台 - 用户钱包")
@RestController @RestController
@ -48,4 +50,21 @@ public class PayWalletController {
return success(PayWalletConvert.INSTANCE.convertPage(pageResult)); return success(PayWalletConvert.INSTANCE.convertPage(pageResult));
} }
@PutMapping("/update-balance")
@Operation(summary = "更新会员用户余额")
@PreAuthorize("@ss.hasPermission('pay:wallet:update-balance')")
public CommonResult<Boolean> updateWalletBalance(@Valid @RequestBody PayWalletUpdateBalanceReqVO updateReqVO) {
// 获得用户钱包
PayWalletDO wallet = payWalletService.getOrCreateWallet(updateReqVO.getUserId(), MEMBER.getValue());
if (wallet == null) {
log.error("[updateWalletBalance]updateReqVO({}) 用户钱包不存在.", updateReqVO);
throw exception(WALLET_NOT_FOUND);
}
// 更新钱包余额
payWalletService.addWalletBalance(wallet.getId(), String.valueOf(updateReqVO.getUserId()),
PayWalletBizTypeEnum.UPDATE_BALANCE, updateReqVO.getBalance());
return success(true);
}
} }

View File

@ -0,0 +1,19 @@
package cn.iocoder.yudao.module.pay.controller.admin.wallet.vo.wallet;
import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.constraints.NotNull;
import lombok.Data;
@Schema(description = "管理后台 - 修改钱包余额 Request VO")
@Data
public class PayWalletUpdateBalanceReqVO {
@Schema(description = "用户编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "23788")
@NotNull(message = "用户编号不能为空")
private Long userId;
@Schema(description = "变动余额,正数为增加,负数为减少", requiredMode = Schema.RequiredMode.REQUIRED, example = "100")
@NotNull(message = "变动余额不能为空")
private Integer balance;
}

View File

@ -12,12 +12,12 @@ import cn.iocoder.yudao.module.pay.enums.wallet.PayWalletBizTypeEnum;
import cn.iocoder.yudao.module.pay.service.order.PayOrderService; import cn.iocoder.yudao.module.pay.service.order.PayOrderService;
import cn.iocoder.yudao.module.pay.service.refund.PayRefundService; import cn.iocoder.yudao.module.pay.service.refund.PayRefundService;
import cn.iocoder.yudao.module.pay.service.wallet.bo.WalletTransactionCreateReqBO; import cn.iocoder.yudao.module.pay.service.wallet.bo.WalletTransactionCreateReqBO;
import jakarta.annotation.Resource;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.springframework.context.annotation.Lazy; import org.springframework.context.annotation.Lazy;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional; import org.springframework.transaction.annotation.Transactional;
import jakarta.annotation.Resource;
import java.time.LocalDateTime; import java.time.LocalDateTime;
import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception; import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception;
@ -176,6 +176,9 @@ public class PayWalletServiceImpl implements PayWalletService {
walletMapper.updateWhenRecharge(payWallet.getId(), price); walletMapper.updateWhenRecharge(payWallet.getId(), price);
break; break;
} }
case UPDATE_BALANCE: // 更新余额
walletMapper.updateWhenRecharge(payWallet.getId(), price);
break;
default: { default: {
// TODO 其它类型待实现 // TODO 其它类型待实现
throw new UnsupportedOperationException("待实现"); throw new UnsupportedOperationException("待实现");

View File

@ -6,9 +6,7 @@ import cn.iocoder.yudao.module.system.framework.sms.core.enums.SmsChannelEnum;
import cn.iocoder.yudao.module.system.service.sms.SmsSendService; import cn.iocoder.yudao.module.system.service.sms.SmsSendService;
import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag; import io.swagger.v3.oas.annotations.tags.Tag;
import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.*;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import jakarta.annotation.Resource; import jakarta.annotation.Resource;
import jakarta.annotation.security.PermitAll; import jakarta.annotation.security.PermitAll;
@ -26,7 +24,7 @@ public class SmsCallbackController {
@PostMapping("/aliyun") @PostMapping("/aliyun")
@PermitAll @PermitAll
@Operation(summary = "阿里云短信的回调", description = "参见 https://help.aliyun.com/zh/sms/developer-reference/configure-delivery-receipts-1 文档") @Operation(summary = "阿里云短信的回调", description = "参见 https://help.aliyun.com/document_detail/120998.html 文档")
public CommonResult<Boolean> receiveAliyunSmsStatus(HttpServletRequest request) throws Throwable { public CommonResult<Boolean> receiveAliyunSmsStatus(HttpServletRequest request) throws Throwable {
String text = ServletUtils.getBody(request); String text = ServletUtils.getBody(request);
smsSendService.receiveSmsStatus(SmsChannelEnum.ALIYUN.getCode(), text); smsSendService.receiveSmsStatus(SmsChannelEnum.ALIYUN.getCode(), text);
@ -35,7 +33,7 @@ public class SmsCallbackController {
@PostMapping("/tencent") @PostMapping("/tencent")
@PermitAll @PermitAll
@Operation(summary = "腾讯云短信的回调", description = "参见 https://cloud.tencent.com/document/product/382/59178 文档") @Operation(summary = "腾讯云短信的回调", description = "参见 https://cloud.tencent.com/document/product/382/52077 文档")
public CommonResult<Boolean> receiveTencentSmsStatus(HttpServletRequest request) throws Throwable { public CommonResult<Boolean> receiveTencentSmsStatus(HttpServletRequest request) throws Throwable {
String text = ServletUtils.getBody(request); String text = ServletUtils.getBody(request);
smsSendService.receiveSmsStatus(SmsChannelEnum.TENCENT.getCode(), text); smsSendService.receiveSmsStatus(SmsChannelEnum.TENCENT.getCode(), text);
@ -46,10 +44,17 @@ public class SmsCallbackController {
@PostMapping("/huawei") @PostMapping("/huawei")
@PermitAll @PermitAll
@Operation(summary = "华为云短信的回调", description = "参见 https://support.huaweicloud.com/api-msgsms/sms_05_0003.html 文档") @Operation(summary = "华为云短信的回调", description = "参见 https://support.huaweicloud.com/api-msgsms/sms_05_0003.html 文档")
public CommonResult<Boolean> receiveHuaweiSmsStatus(HttpServletRequest request) throws Throwable { public CommonResult<Boolean> receiveHuaweiSmsStatus(@RequestBody String requestBody) throws Throwable {
String text = ServletUtils.getBody(request); smsSendService.receiveSmsStatus(SmsChannelEnum.HUAWEI.getCode(), requestBody);
smsSendService.receiveSmsStatus(SmsChannelEnum.HUAWEI.getCode(), text);
return success(true); return success(true);
} }
} @PostMapping("/qiniu")
@PermitAll
@Operation(summary = "七牛云短信的回调", description = "参见 https://developer.qiniu.com/sms/5910/message-push 文档")
public CommonResult<Boolean> receiveQiniuSmsStatus(@RequestBody String requestBody) throws Throwable {
smsSendService.receiveSmsStatus(SmsChannelEnum.QINIU.getCode(), requestBody);
return success(true);
}
}

View File

@ -30,7 +30,8 @@ public interface SmsClientFactory {
* 创建短信 Client * 创建短信 Client
* *
* @param properties 配置对象 * @param properties 配置对象
* @return 短信 Client
*/ */
void createOrUpdateSmsClient(SmsChannelProperties properties); SmsClient createOrUpdateSmsClient(SmsChannelProperties properties);
} }

View File

@ -26,15 +26,9 @@ public abstract class AbstractSmsClient implements SmsClient {
* 初始化 * 初始化
*/ */
public final void init() { public final void init() {
doInit();
log.debug("[init][配置({}) 初始化完成]", properties); log.debug("[init][配置({}) 初始化完成]", properties);
} }
/**
* 自定义初始化
*/
protected abstract void doInit();
public final void refresh(SmsChannelProperties properties) { public final void refresh(SmsChannelProperties properties) {
// 判断是否更新 // 判断是否更新
if (properties.equals(this.properties)) { if (properties.equals(this.properties)) {

View File

@ -50,10 +50,6 @@ public class AliyunSmsClient extends AbstractSmsClient {
Assert.notEmpty(properties.getApiSecret(), "apiSecret 不能为空"); Assert.notEmpty(properties.getApiSecret(), "apiSecret 不能为空");
} }
@Override
protected void doInit() {
}
@Override @Override
public SmsSendRespDTO sendSms(Long sendLogId, String mobile, String apiTemplateId, public SmsSendRespDTO sendSms(Long sendLogId, String mobile, String apiTemplateId,
List<KeyValue<String, Object>> templateParams) throws Throwable { List<KeyValue<String, Object>> templateParams) throws Throwable {
@ -80,7 +76,7 @@ public class AliyunSmsClient extends AbstractSmsClient {
@Override @Override
public List<SmsReceiveRespDTO> parseSmsReceiveStatus(String text) { public List<SmsReceiveRespDTO> parseSmsReceiveStatus(String text) {
JSONArray statuses = JSONUtil.parseArray(text); JSONArray statuses = JSONUtil.parseArray(text);
// 字段参考 // 字段参考 https://help.aliyun.com/zh/sms/developer-reference/smsreport-2
return convertList(statuses, status -> { return convertList(statuses, status -> {
JSONObject statusObj = (JSONObject) status; JSONObject statusObj = (JSONObject) status;
return new SmsReceiveRespDTO() return new SmsReceiveRespDTO()
@ -166,7 +162,8 @@ public class AliyunSmsClient extends AbstractSmsClient {
String hashedRequestBody = DigestUtil.sha256Hex(requestBody); String hashedRequestBody = DigestUtil.sha256Hex(requestBody);
// 4. 构建 Authorization 签名 // 4. 构建 Authorization 签名
String canonicalRequest = "POST" + "\n" + "/" + "\n" + queryString + "\n" + canonicalHeaders + "\n" + signedHeaders + "\n" + hashedRequestBody; String canonicalRequest = "POST" + "\n" + "/" + "\n" + queryString + "\n"
+ canonicalHeaders + "\n" + signedHeaders + "\n" + hashedRequestBody;
String hashedCanonicalRequest = DigestUtil.sha256Hex(canonicalRequest); String hashedCanonicalRequest = DigestUtil.sha256Hex(canonicalRequest);
String stringToSign = "ACS3-HMAC-SHA256" + "\n" + hashedCanonicalRequest; String stringToSign = "ACS3-HMAC-SHA256" + "\n" + hashedCanonicalRequest;
String signature = SecureUtil.hmacSha256(properties.getApiSecret()).digestHex(stringToSign); // 计算签名 String signature = SecureUtil.hmacSha256(properties.getApiSecret()).digestHex(stringToSign); // 计算签名

View File

@ -36,10 +36,6 @@ public class DebugDingTalkSmsClient extends AbstractSmsClient {
Assert.notEmpty(properties.getApiSecret(), "apiSecret 不能为空"); Assert.notEmpty(properties.getApiSecret(), "apiSecret 不能为空");
} }
@Override
protected void doInit() {
}
@Override @Override
public SmsSendRespDTO sendSms(Long sendLogId, String mobile, public SmsSendRespDTO sendSms(Long sendLogId, String mobile,
String apiTemplateId, List<KeyValue<String, Object>> templateParams) throws Throwable { String apiTemplateId, List<KeyValue<String, Object>> templateParams) throws Throwable {

View File

@ -1,41 +1,35 @@
package cn.iocoder.yudao.module.system.framework.sms.core.client.impl; package cn.iocoder.yudao.module.system.framework.sms.core.client.impl;
import cn.hutool.core.collection.ListUtil;
import cn.hutool.core.date.format.FastDateFormat;
import cn.hutool.core.lang.Assert; import cn.hutool.core.lang.Assert;
import cn.hutool.core.util.CharsetUtil;
import cn.hutool.core.util.StrUtil; import cn.hutool.core.util.StrUtil;
import cn.hutool.crypto.SecureUtil; import cn.hutool.crypto.SecureUtil;
import cn.hutool.http.HttpRequest; import cn.hutool.http.HttpUtil;
import cn.hutool.http.HttpResponse;
import cn.hutool.json.JSONObject; import cn.hutool.json.JSONObject;
import cn.hutool.json.JSONUtil; import cn.hutool.json.JSONUtil;
import cn.iocoder.yudao.framework.common.core.KeyValue; import cn.iocoder.yudao.framework.common.core.KeyValue;
import cn.iocoder.yudao.framework.common.util.collection.CollectionUtils; import cn.iocoder.yudao.framework.common.util.http.HttpUtils;
import cn.iocoder.yudao.framework.common.util.json.JsonUtils; import cn.iocoder.yudao.framework.common.util.json.JsonUtils;
import cn.iocoder.yudao.module.system.framework.sms.core.client.dto.SmsReceiveRespDTO; import cn.iocoder.yudao.module.system.framework.sms.core.client.dto.SmsReceiveRespDTO;
import cn.iocoder.yudao.module.system.framework.sms.core.client.dto.SmsSendRespDTO; import cn.iocoder.yudao.module.system.framework.sms.core.client.dto.SmsSendRespDTO;
import cn.iocoder.yudao.module.system.framework.sms.core.client.dto.SmsTemplateRespDTO; import cn.iocoder.yudao.module.system.framework.sms.core.client.dto.SmsTemplateRespDTO;
import cn.iocoder.yudao.module.system.framework.sms.core.enums.SmsTemplateAuditStatusEnum; import cn.iocoder.yudao.module.system.framework.sms.core.enums.SmsTemplateAuditStatusEnum;
import cn.iocoder.yudao.module.system.framework.sms.core.property.SmsChannelProperties; import cn.iocoder.yudao.module.system.framework.sms.core.property.SmsChannelProperties;
import com.fasterxml.jackson.annotation.JsonFormat;
import com.fasterxml.jackson.annotation.JsonProperty;
import lombok.Data;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import java.io.UnsupportedEncodingException; import java.io.UnsupportedEncodingException;
import java.net.URLEncoder; import java.net.URLEncoder;
import java.text.SimpleDateFormat; import java.nio.charset.StandardCharsets;
import java.util.*; import java.time.Instant;
import java.time.LocalDateTime; import java.time.LocalDateTime;
import java.time.ZoneId;
import java.util.*;
import static cn.hutool.crypto.digest.DigestUtil.sha256Hex; import static cn.hutool.crypto.digest.DigestUtil.sha256Hex;
import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertList; import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertList;
import static cn.iocoder.yudao.framework.common.util.date.DateUtils.FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND;
import static cn.iocoder.yudao.framework.common.util.date.DateUtils.TIME_ZONE_DEFAULT;
// todo @scholar参考阿里云在优化下
/** /**
* 华为短信客户端的实现类 * 华为短信客户端的实现类
* *
@ -45,182 +39,128 @@ import static cn.iocoder.yudao.framework.common.util.date.DateUtils.TIME_ZONE_DE
@Slf4j @Slf4j
public class HuaweiSmsClient extends AbstractSmsClient { public class HuaweiSmsClient extends AbstractSmsClient {
/** private static final String URL = "https://smsapi.cn-north-4.myhuaweicloud.com:443/sms/batchSendSms/v1";//APP接入地址+接口访问URI
* 调用成功 code private static final String HOST = "smsapi.cn-north-4.myhuaweicloud.com:443";
*/ private static final String SIGNEDHEADERS = "content-type;host;x-sdk-date";
public static final String URL = "https://smsapi.cn-north-4.myhuaweicloud.com:443/sms/batchSendSms/v1";//APP接入地址+接口访问URI
public static final String HOST = "smsapi.cn-north-4.myhuaweicloud.com:443";
public static final String SIGNEDHEADERS = "content-type;host;x-sdk-date";
@Override private static final String RESPONSE_CODE_SUCCESS = "000000";
protected void doInit() {
}
public HuaweiSmsClient(SmsChannelProperties properties) { public HuaweiSmsClient(SmsChannelProperties properties) {
super(properties); super(properties);
Assert.notEmpty(properties.getApiKey(), "apiKey 不能为空"); Assert.notEmpty(properties.getApiKey(), "apiKey 不能为空");
Assert.notEmpty(properties.getApiSecret(), "apiSecret 不能为空"); Assert.notEmpty(properties.getApiSecret(), "apiSecret 不能为空");
validateSender(properties);
}
/**
* 参数校验华为云的 sender 通道号
*
* 原因是验华为云发放短信的时候需要额外的参数 sender
*
* 解决方案考虑到不破坏原有的 apiKey + apiSecret 的结构所以将 secretId 拼接到 apiKey 字段中格式为 "secretId sdkAppId"
*
* @param properties 配置
*/
private static void validateSender(SmsChannelProperties properties) {
String combineKey = properties.getApiKey();
Assert.notEmpty(combineKey, "apiKey 不能为空");
String[] keys = combineKey.trim().split(" ");
Assert.isTrue(keys.length == 2, "华为云短信 apiKey 配置格式错误,请配置 为[accessKeyId sender]");
}
private String getAccessKey() {
return StrUtil.subBefore(properties.getApiKey(), " ", true);
}
private String getSender() {
return StrUtil.subAfter(properties.getApiKey(), " ", true);
} }
@Override @Override
public SmsSendRespDTO sendSms(Long sendLogId, String mobile, String apiTemplateId, public SmsSendRespDTO sendSms(Long sendLogId, String mobile, String apiTemplateId,
List<KeyValue<String, Object>> templateParams) throws Throwable { List<KeyValue<String, Object>> templateParams) throws Throwable {
// 参考链接 https://support.huaweicloud.com/api-msgsms/sms_05_0001.html StringBuilder requestBody = new StringBuilder();
// 相比较阿里短信华为短信发送的时候需要额外的参数通道号考虑到不破坏原有的的结构 appendToBody(requestBody, "from=", getSender());
// 所以将 通道号 拼接到 apiTemplateId 字段中格式为 "apiTemplateId 通道号"空格为分隔符 appendToBody(requestBody, "&to=", mobile);
String sender = StrUtil.subAfter(apiTemplateId, " ", true); //中国大陆短信签名通道号或全球短信通道号 appendToBody(requestBody, "&templateId=", apiTemplateId);
String templateId = StrUtil.subBefore(apiTemplateId, " ", true); //模板ID appendToBody(requestBody, "&templateParas=", JsonUtils.toJsonString(
convertList(templateParams, kv -> String.valueOf(kv.getValue()))));
appendToBody(requestBody, "&statusCallback=", properties.getCallbackUrl());
appendToBody(requestBody, "&extend=", String.valueOf(sendLogId));
JSONObject response = request("/sms/batchSendSms/v1/", "POST", requestBody.toString());
//选填,短信状态报告接收地址,推荐使用域名,为空或者不填表示不接收状态报告 // 2. 解析请求
String statusCallBack = properties.getCallbackUrl(); if (!response.containsKey("result")) { // 例如说密钥不正确
return new SmsSendRespDTO().setSuccess(false)
List<String> templateParas = CollectionUtils.convertList(templateParams, kv -> String.valueOf(kv.getValue())); .setApiCode(response.getStr("code"))
.setApiMsg(response.getStr("description"));
JSONObject JsonResponse = sendSmsRequest(sender,mobile,templateId,templateParas,statusCallBack); }
SmsResponse smsResponse = getSmsSendResponse(JsonResponse); JSONObject sendResult = response.getJSONArray("result").getJSONObject(0);
return new SmsSendRespDTO().setSuccess(RESPONSE_CODE_SUCCESS.equals(response.getStr("code")))
return new SmsSendRespDTO().setSuccess(smsResponse.success).setApiMsg(smsResponse.data.toString()); .setSerialNo(sendResult.getStr("smsMsgId")).setApiCode(sendResult.getStr("status"));
} }
JSONObject sendSmsRequest(String sender,String mobile,String templateId,List<String> templateParas,String statusCallBack) throws UnsupportedEncodingException { /**
* 请求华为云短信
*
* @see <a href="认证鉴权">https://support.huaweicloud.com/api-msgsms/sms_05_0046.html</a>
* @param uri 请求 URI
* @param method 请求 Method
* @param requestBody 请求 Body
* @return 请求结果
*/
private JSONObject request(String uri, String method, String requestBody) {
// 1.1 请求 Header
TreeMap<String, String> headers = new TreeMap<>();
headers.put("Content-Type", "application/x-www-form-urlencoded");
String sdkDate = FastDateFormat.getInstance("yyyyMMdd'T'HHmmss'Z'", TimeZone.getTimeZone("UTC")).format(new Date());
headers.put("X-Sdk-Date", sdkDate);
headers.put("host", HOST);
SimpleDateFormat sdf = new SimpleDateFormat("yyyyMMdd'T'HHmmss'Z'", Locale.ENGLISH); // 1.2 构建签名 Header
sdf.setTimeZone(TimeZone.getTimeZone("UTC")); String canonicalQueryString = ""; // 查询参数为空
String sdkDate = sdf.format(new Date());
// ************* 步骤 1拼接规范请求串 *************
String httpRequestMethod = "POST";
String canonicalUri = "/sms/batchSendSms/v1/";
String canonicalQueryString = "";//查询参数为空
String canonicalHeaders = "content-type:application/x-www-form-urlencoded\n" String canonicalHeaders = "content-type:application/x-www-form-urlencoded\n"
+ "host:"+ HOST +"\n" + "host:"+ HOST +"\n" + "x-sdk-date:" + sdkDate + "\n";
+ "x-sdk-date:" + sdkDate + "\n"; String canonicalRequest = method + "\n" + uri + "\n" + canonicalQueryString + "\n"
//请求Body,不携带签名名称时,signature请填null + canonicalHeaders + "\n" + SIGNEDHEADERS + "\n" + sha256Hex(requestBody);
String body = buildRequestBody(sender, mobile, templateId, templateParas, statusCallBack, null); String stringToSign = "SDK-HMAC-SHA256" + "\n" + sdkDate + "\n" + sha256Hex(canonicalRequest);
if (null == body || body.isEmpty()) { String signature = SecureUtil.hmacSha256(properties.getApiSecret()).digestHex(stringToSign); // 计算签名
return null; headers.put("Authorization", "SDK-HMAC-SHA256" + " " + "Access=" + getAccessKey()
} + ", " + "SignedHeaders=" + SIGNEDHEADERS + ", " + "Signature=" + signature);
String hashedRequestBody = sha256Hex(body);
String canonicalRequest = httpRequestMethod + "\n" + canonicalUri + "\n" + canonicalQueryString + "\n"
+ canonicalHeaders + "\n" + SIGNEDHEADERS + "\n" + hashedRequestBody;
// ************* 步骤 2拼接待签名字符串 ************* // 2. 发起请求
String hashedCanonicalRequest = sha256Hex(canonicalRequest); String responseBody = HttpUtils.post(URL, headers, requestBody);
String stringToSign = "SDK-HMAC-SHA256" + "\n" + sdkDate + "\n" + hashedCanonicalRequest; return JSONUtil.parseObj(responseBody);
// ************* 步骤 3计算签名 *************
String signature = SecureUtil.hmacSha256(properties.getApiSecret()).digestHex(stringToSign);
// ************* 步骤 4拼接 Authorization *************
String authorization = "SDK-HMAC-SHA256" + " " + "Access=" + properties.getApiKey() + ", "
+ "SignedHeaders=" + SIGNEDHEADERS + ", " + "Signature=" + signature;
// ************* 步骤 5构造HttpRequest 并执行request请求获得response *************
HttpResponse response = HttpRequest.post(URL)
.header("Content-Type", "application/x-www-form-urlencoded")
.header("X-Sdk-Date", sdkDate)
.header("host",HOST)
.header("Authorization", authorization)
.body(body)
.execute();
return JSONUtil.parseObj(response.body());
} }
private SmsResponse getSmsSendResponse(JSONObject resJson) {
SmsResponse smsResponse = new SmsResponse();
smsResponse.setSuccess("000000".equals(resJson.getStr("code")));
smsResponse.setData(resJson);
return smsResponse;
}
static String buildRequestBody(String sender, String receiver, String templateId, List<String> templateParas,
String statusCallBack, String signature) throws UnsupportedEncodingException {
if (null == sender || null == receiver || null == templateId || sender.isEmpty() || receiver.isEmpty()
|| templateId.isEmpty()) {
System.out.println("buildRequestBody(): sender, receiver or templateId is null.");
return null;
}
StringBuilder body = new StringBuilder();
appendToBody(body, "from=", sender);
appendToBody(body, "&to=", receiver);
appendToBody(body, "&templateId=", templateId);
appendToBody(body, "&templateParas=", JsonUtils.toJsonString(templateParas));
appendToBody(body, "&statusCallback=", statusCallBack);
appendToBody(body, "&signature=", signature);
return body.toString();
}
private static void appendToBody(StringBuilder body, String key, String val) throws UnsupportedEncodingException {
if (null != val && !val.isEmpty()) {
body.append(key).append(URLEncoder.encode(val, "UTF-8"));
}
}
@Override @Override
public List<SmsReceiveRespDTO> parseSmsReceiveStatus(String text) { public List<SmsReceiveRespDTO> parseSmsReceiveStatus(String requestBody) {
List<SmsReceiveStatus> statuses = JsonUtils.parseArray(text, SmsReceiveStatus.class); Map<String, String> params = HttpUtil.decodeParamMap(requestBody, StandardCharsets.UTF_8);
return convertList(statuses, status -> new SmsReceiveRespDTO().setSuccess(Objects.equals(status.getStatus(),"DELIVRD")) // 字段参考 https://support.huaweicloud.com/api-msgsms/sms_05_0003.html
.setErrorCode(status.getStatus()).setErrorMsg(status.getStatus()) return ListUtil.of(new SmsReceiveRespDTO()
.setMobile(status.getPhoneNumber()).setReceiveTime(status.getUpdateTime()) .setSuccess("DELIVRD".equals(params.get("status"))) // 是否接收成功
.setSerialNo(status.getSmsMsgId())); .setErrorCode(params.get("status")) // 状态报告编码
.setErrorMsg(params.get("statusDesc"))
.setMobile(params.get("to")) // 手机号
.setReceiveTime(LocalDateTime.ofInstant(Instant.parse(params.get("updateTime")), ZoneId.of("UTC"))) // 状态报告时间
.setSerialNo(params.get("smsMsgId")) // 发送序列号
.setLogId(Long.valueOf(params.get("extend")))); // 用户序列号
} }
@Override @Override
public SmsTemplateRespDTO getSmsTemplate(String apiTemplateId) throws Throwable { public SmsTemplateRespDTO getSmsTemplate(String apiTemplateId) throws Throwable {
//华为短信模板查询和发送短信是不同的两套key和secret与阿里腾讯的区别较大这里模板查询校验暂不实现 // 华为短信模板查询和发送短信是不同的两套 key secret与阿里腾讯的区别较大这里模板查询校验暂不实现
return new SmsTemplateRespDTO().setId(null).setContent(null) String[] strs = apiTemplateId.split(" ");
Assert.isTrue(strs.length == 2, "格式不正确需要满足apiTemplateId sender");
return new SmsTemplateRespDTO().setId(apiTemplateId).setContent(null)
.setAuditStatus(SmsTemplateAuditStatusEnum.SUCCESS.getStatus()).setAuditReason(null); .setAuditStatus(SmsTemplateAuditStatusEnum.SUCCESS.getStatus()).setAuditReason(null);
} }
@Data @SuppressWarnings("CharsetObjectCanBeUsed")
public static class SmsResponse { private static void appendToBody(StringBuilder body, String key, String value) throws UnsupportedEncodingException {
if (StrUtil.isNotEmpty(value)) {
/** body.append(key).append(URLEncoder.encode(value, CharsetUtil.CHARSET_UTF_8.name()));
* 是否成功 }
*/
private boolean success;
/**
* 厂商原返回体
*/
private Object data;
} }
}
/**
* 短信接收状态
*
* 参见 <a href="https://support.huaweicloud.com/api-msgsms/sms_05_0003.html">文档</a>
*
* @author scholar
*/
@Data
public static class SmsReceiveStatus {
/**
* 本条状态报告对应的短信的接收方号码仅当状态报告中携带了extend参数时才会同时携带该参数
*/
@JsonProperty("to")
private String phoneNumber;
/**
* 短信资源的更新时间通常为短信平台接收短信状态报告的时间
*/
@JsonFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND, timezone = TIME_ZONE_DEFAULT)
private LocalDateTime updateTime;
/**
* 短信状态报告枚举值
*/
private String status;
/**
* 发送短信成功时返回的短信唯一标识
*/
private String smsMsgId;
}
}

View File

@ -0,0 +1,155 @@
package cn.iocoder.yudao.module.system.framework.sms.core.client.impl;
import cn.hutool.core.collection.CollStreamUtil;
import cn.hutool.core.date.DateUtil;
import cn.hutool.core.date.LocalDateTimeUtil;
import cn.hutool.core.lang.Assert;
import cn.hutool.core.util.ObjectUtil;
import cn.hutool.crypto.SecureUtil;
import cn.hutool.crypto.digest.HmacAlgorithm;
import cn.hutool.json.JSONObject;
import cn.hutool.json.JSONUtil;
import cn.iocoder.yudao.framework.common.core.KeyValue;
import cn.iocoder.yudao.framework.common.util.http.HttpUtils;
import cn.iocoder.yudao.module.system.framework.sms.core.client.dto.SmsReceiveRespDTO;
import cn.iocoder.yudao.module.system.framework.sms.core.client.dto.SmsSendRespDTO;
import cn.iocoder.yudao.module.system.framework.sms.core.client.dto.SmsTemplateRespDTO;
import cn.iocoder.yudao.module.system.framework.sms.core.enums.SmsTemplateAuditStatusEnum;
import cn.iocoder.yudao.module.system.framework.sms.core.property.SmsChannelProperties;
import com.google.common.annotations.VisibleForTesting;
import lombok.extern.slf4j.Slf4j;
import java.util.*;
import java.util.function.Function;
import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertList;
/**
* 七牛云短信客户端的实现类
*
* @author scholar
* @since 2024/08/26 15:35
*/
@Slf4j
public class QiniuSmsClient extends AbstractSmsClient {
private static final String HOST = "sms.qiniuapi.com";
public QiniuSmsClient(SmsChannelProperties properties) {
super(properties);
Assert.notEmpty(properties.getApiKey(), "apiKey 不能为空");
Assert.notEmpty(properties.getApiSecret(), "apiSecret 不能为空");
}
public SmsSendRespDTO sendSms(Long sendLogId, String mobile, String apiTemplateId,
List<KeyValue<String, Object>> templateParams) throws Throwable {
// 1. 执行请求
// 参考链接 https://developer.qiniu.com/sms/5824/through-the-api-send-text-messages
LinkedHashMap<String, Object> body = new LinkedHashMap<>();
body.put("template_id", apiTemplateId);
body.put("mobile", mobile);
body.put("parameters", CollStreamUtil.toMap(templateParams, KeyValue::getKey, KeyValue::getValue));
body.put("seq", Long.toString(sendLogId));
JSONObject response = request("POST", body, "/v1/message/single");
// 2. 解析请求
if (ObjectUtil.isNotEmpty(response.getStr("error"))) {
// 短信请求失败
return new SmsSendRespDTO().setSuccess(false)
.setApiCode(response.getStr("error"))
.setApiRequestId(response.getStr("request_id"))
.setApiMsg(response.getStr("message"));
}
return new SmsSendRespDTO().setSuccess(response.containsKey("message_id"))
.setSerialNo(response.getStr("message_id"));
}
/**
* 请求七牛云短信
*
* @see <a href="https://developer.qiniu.com/sms/5842/sms-api-authentication"</>
* @param httpMethod http请求方法
* @param body http请求消息体
* @param path URL path
* @return 请求结果
*/
private JSONObject request(String httpMethod, LinkedHashMap<String, Object> body, String path) {
String signDate = DateUtil.date().setTimeZone(TimeZone.getTimeZone("UTC")).toString("yyyyMMdd'T'HHmmss'Z'");
// 1. 请求头
Map<String, String> header = new HashMap<>(4);
header.put("HOST", HOST);
header.put("Authorization", getSignature(httpMethod, path, body != null ? JSONUtil.toJsonStr(body) : "", signDate));
header.put("Content-Type", "application/json");
header.put("X-Qiniu-Date", signDate);
// 2. 发起请求
String responseBody;
if (Objects.equals(httpMethod, "POST")){
responseBody = HttpUtils.post("https://" + HOST + path, header, JSONUtil.toJsonStr(body));
} else {
responseBody = HttpUtils.get("https://" + HOST + path, header);
}
return JSONUtil.parseObj(responseBody);
}
private String getSignature(String method, String path, String body, String signDate) {
StringBuilder dataToSign = new StringBuilder();
dataToSign.append(method.toUpperCase()).append(" ").append(path)
.append("\nHost: ").append(HOST)
.append("\n").append("Content-Type").append(": ").append("application/json")
.append("\n").append("X-Qiniu-Date").append(": ").append(signDate)
.append("\n\n");
if (ObjectUtil.isNotEmpty(body)) {
dataToSign.append(body);
}
String signature = SecureUtil.hmac(HmacAlgorithm.HmacSHA1, properties.getApiSecret())
.digestBase64(dataToSign.toString(), true);
return "Qiniu " + properties.getApiKey() + ":" + signature;
}
@Override
public List<SmsReceiveRespDTO> parseSmsReceiveStatus(String text) {
JSONObject status = JSONUtil.parseObj(text);
// 字段参考 https://developer.qiniu.com/sms/5910/message-push
return convertList(status.getJSONArray("items"), new Function<Object, SmsReceiveRespDTO>() {
@Override
public SmsReceiveRespDTO apply(Object item) {
JSONObject statusObj = (JSONObject) item;
return new SmsReceiveRespDTO()
.setSuccess("DELIVRD".equals(statusObj.getStr("status"))) // 是否接收成功
.setErrorMsg(statusObj.getStr("status")) // 状态报告编码
.setMobile(statusObj.getStr("mobile")) // 手机号
.setReceiveTime(LocalDateTimeUtil.of(statusObj.getLong("delivrd_at") * 1000L)) // 状态报告时间
.setSerialNo(statusObj.getStr("message_id")) // 发送序列号
.setLogId(statusObj.getLong("seq")); // 用户序列号
}
});
}
@Override
public SmsTemplateRespDTO getSmsTemplate(String apiTemplateId) throws Throwable {
// 1. 执行请求
// 参考链接 https://developer.qiniu.com/sms/5969/query-a-single-template
JSONObject response = request("GET", null, "/v1/template/" + apiTemplateId);
// 2.2 解析请求
return new SmsTemplateRespDTO()
.setId(response.getStr("id"))
.setContent(response.getStr("template"))
.setAuditStatus(convertSmsTemplateAuditStatus(response.getStr("audit_status")))
.setAuditReason(response.getStr("reject_reason"));
}
@VisibleForTesting
Integer convertSmsTemplateAuditStatus(String templateStatus) {
switch (templateStatus) {
case "passed": return SmsTemplateAuditStatusEnum.SUCCESS.getStatus();
case "reviewing": return SmsTemplateAuditStatusEnum.CHECKING.getStatus();
case "rejected": return SmsTemplateAuditStatusEnum.FAIL.getStatus();
default:
throw new IllegalArgumentException(String.format("未知审核状态(%str)", templateStatus));
}
}
}

View File

@ -59,7 +59,7 @@ public class SmsClientFactoryImpl implements SmsClientFactory {
} }
@Override @Override
public void createOrUpdateSmsClient(SmsChannelProperties properties) { public SmsClient createOrUpdateSmsClient(SmsChannelProperties properties) {
AbstractSmsClient client = channelIdClients.get(properties.getId()); AbstractSmsClient client = channelIdClients.get(properties.getId());
if (client == null) { if (client == null) {
client = this.createSmsClient(properties); client = this.createSmsClient(properties);
@ -68,6 +68,7 @@ public class SmsClientFactoryImpl implements SmsClientFactory {
} else { } else {
client.refresh(properties); client.refresh(properties);
} }
return client;
} }
private AbstractSmsClient createSmsClient(SmsChannelProperties properties) { private AbstractSmsClient createSmsClient(SmsChannelProperties properties) {
@ -79,6 +80,7 @@ public class SmsClientFactoryImpl implements SmsClientFactory {
case DEBUG_DING_TALK: return new DebugDingTalkSmsClient(properties); case DEBUG_DING_TALK: return new DebugDingTalkSmsClient(properties);
case TENCENT: return new TencentSmsClient(properties); case TENCENT: return new TencentSmsClient(properties);
case HUAWEI: return new HuaweiSmsClient(properties); case HUAWEI: return new HuaweiSmsClient(properties);
case QINIU: return new QiniuSmsClient(properties);
} }
// 创建失败错误日志 + 抛出异常 // 创建失败错误日志 + 抛出异常
log.error("[createSmsClient][配置({}) 找不到合适的客户端实现]", properties); log.error("[createSmsClient][配置({}) 找不到合适的客户端实现]", properties);

View File

@ -1,7 +1,11 @@
package cn.iocoder.yudao.module.system.framework.sms.core.client.impl; package cn.iocoder.yudao.module.system.framework.sms.core.client.impl;
import cn.hutool.core.date.format.FastDateFormat;
import cn.hutool.core.lang.Assert; import cn.hutool.core.lang.Assert;
import cn.hutool.core.util.HexUtil;
import cn.hutool.core.util.StrUtil; import cn.hutool.core.util.StrUtil;
import cn.hutool.crypto.digest.DigestUtil;
import cn.hutool.crypto.digest.HmacAlgorithm;
import cn.hutool.json.JSONArray; import cn.hutool.json.JSONArray;
import cn.hutool.json.JSONObject; import cn.hutool.json.JSONObject;
import cn.hutool.json.JSONUtil; import cn.hutool.json.JSONUtil;
@ -14,12 +18,8 @@ import cn.iocoder.yudao.module.system.framework.sms.core.client.dto.SmsTemplateR
import cn.iocoder.yudao.module.system.framework.sms.core.enums.SmsTemplateAuditStatusEnum; import cn.iocoder.yudao.module.system.framework.sms.core.enums.SmsTemplateAuditStatusEnum;
import cn.iocoder.yudao.module.system.framework.sms.core.property.SmsChannelProperties; import cn.iocoder.yudao.module.system.framework.sms.core.property.SmsChannelProperties;
import com.google.common.annotations.VisibleForTesting; import com.google.common.annotations.VisibleForTesting;
import jakarta.xml.bind.DatatypeConverter;
import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;
import java.nio.charset.StandardCharsets; import java.nio.charset.StandardCharsets;
import java.text.SimpleDateFormat;
import java.util.*; import java.util.*;
import static cn.hutool.crypto.digest.DigestUtil.sha256Hex; import static cn.hutool.crypto.digest.DigestUtil.sha256Hex;
@ -34,6 +34,7 @@ import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.
*/ */
public class TencentSmsClient extends AbstractSmsClient { public class TencentSmsClient extends AbstractSmsClient {
private static final String HOST = "sms.tencentcloudapi.com";
private static final String VERSION = "2021-01-11"; private static final String VERSION = "2021-01-11";
private static final String REGION = "ap-guangzhou"; private static final String REGION = "ap-guangzhou";
@ -56,10 +57,6 @@ public class TencentSmsClient extends AbstractSmsClient {
validateSdkAppId(properties); validateSdkAppId(properties);
} }
@Override
protected void doInit() {
}
/** /**
* 参数校验腾讯云的 SDK AppId * 参数校验腾讯云的 SDK AppId
* *
@ -93,7 +90,7 @@ public class TencentSmsClient extends AbstractSmsClient {
body.put("PhoneNumberSet", new String[]{mobile}); body.put("PhoneNumberSet", new String[]{mobile});
body.put("SmsSdkAppId", getSdkAppId()); body.put("SmsSdkAppId", getSdkAppId());
body.put("SignName", properties.getSignature()); body.put("SignName", properties.getSignature());
body.put("TemplateId",apiTemplateId); body.put("TemplateId", apiTemplateId);
body.put("TemplateParamSet", ArrayUtils.toArray(templateParams, param -> String.valueOf(param.getValue()))); body.put("TemplateParamSet", ArrayUtils.toArray(templateParams, param -> String.valueOf(param.getValue())));
JSONObject response = request("SendSms", body); JSONObject response = request("SendSms", body);
@ -106,11 +103,11 @@ public class TencentSmsClient extends AbstractSmsClient {
.setApiCode(error.getStr("Code")) .setApiCode(error.getStr("Code"))
.setApiMsg(error.getStr("Message")); .setApiMsg(error.getStr("Message"));
} }
JSONObject responseData = responseResult.getJSONArray("SendStatusSet").getJSONObject(0); JSONObject sendResult = responseResult.getJSONArray("SendStatusSet").getJSONObject(0);
return new SmsSendRespDTO().setSuccess(Objects.equals(API_CODE_SUCCESS, responseData.getStr("Code"))) return new SmsSendRespDTO().setSuccess(Objects.equals(API_CODE_SUCCESS, sendResult.getStr("Code")))
.setApiRequestId(responseResult.getStr("RequestId")) .setApiRequestId(responseResult.getStr("RequestId"))
.setSerialNo(responseData.getStr("SerialNo")) .setSerialNo(sendResult.getStr("SerialNo"))
.setApiMsg(responseData.getStr("Message")); .setApiMsg(sendResult.getStr("Message"));
} }
@Override @Override
@ -137,14 +134,13 @@ public class TencentSmsClient extends AbstractSmsClient {
body.put("TemplateIdSet", new Integer[]{Integer.valueOf(apiTemplateId)}); body.put("TemplateIdSet", new Integer[]{Integer.valueOf(apiTemplateId)});
JSONObject response = request("DescribeSmsTemplateList", body); JSONObject response = request("DescribeSmsTemplateList", body);
// TODO @scholar会有请求失败的情况么类似发送的那块逻辑我补充了 // 2. 解析请求
JSONObject TemplateStatusSet = response.getJSONObject("Response").getJSONArray("DescribeTemplateStatusSet").getJSONObject(0); JSONObject statusResult = response.getJSONObject("Response")
String content = TemplateStatusSet.get("TemplateContent").toString(); .getJSONArray("DescribeTemplateStatusSet").getJSONObject(0);
int templateStatus = Integer.parseInt(TemplateStatusSet.get("StatusCode").toString()); return new SmsTemplateRespDTO().setId(apiTemplateId)
String auditReason = TemplateStatusSet.get("ReviewReply").toString(); .setContent(statusResult.get("TemplateContent").toString())
.setAuditStatus(convertSmsTemplateAuditStatus(statusResult.getInt("StatusCode")))
return new SmsTemplateRespDTO().setId(apiTemplateId).setContent(content) .setAuditReason(statusResult.get("ReviewReply").toString());
.setAuditStatus(convertSmsTemplateAuditStatus(templateStatus)).setAuditReason(auditReason);
} }
@VisibleForTesting @VisibleForTesting
@ -166,64 +162,40 @@ public class TencentSmsClient extends AbstractSmsClient {
* @param body 请求参数 * @param body 请求参数
* @return 请求结果 * @return 请求结果
*/ */
private JSONObject request(String action, TreeMap<String, Object> body) throws Exception { private JSONObject request(String action, TreeMap<String, Object> body) {
String timestamp = String.valueOf(System.currentTimeMillis() / 1000); // 1.1 请求 Header
// TODO @scholar这个 format看看怎么写的可以简化点
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd");
// 注意时区否则容易出错
sdf.setTimeZone(TimeZone.getTimeZone("UTC"));
String date = sdf.format(new Date(Long.valueOf(timestamp + "000")));
// TODO @scholar这个步骤看看怎么参考阿里云 client归类下1. 2.1 2.2 这种
// ************* 步骤 1拼接规范请求串 *************
// TODO @scholar这个 hsot 枚举下
String host = "sms.tencentcloudapi.com"; //APP接入地址+接口访问URI
String httpMethod = "POST"; // 请求方式
String canonicalUri = "/";
String canonicalQueryString = "";
String canonicalHeaders = "content-type:application/json; charset=utf-8\n"
+ "host:" + host + "\n" + "x-tc-action:" + action.toLowerCase() + "\n";
String signedHeaders = "content-type;host;x-tc-action";
String hashedRequestBody = sha256Hex(JSONUtil.toJsonStr(body));
// TODO @scholar换行下不然单行太长了
String canonicalRequest = httpMethod + "\n" + canonicalUri + "\n" + canonicalQueryString + "\n" + canonicalHeaders + "\n" + signedHeaders + "\n" + hashedRequestBody;
// ************* 步骤 2拼接待签名字符串 *************
String credentialScope = date + "/" + "sms" + "/" + "tc3_request";
String hashedCanonicalRequest = sha256Hex(canonicalRequest);
String stringToSign = "TC3-HMAC-SHA256" + "\n" + timestamp + "\n" + credentialScope + "\n" + hashedCanonicalRequest;
// ************* 步骤 3计算签名 *************
byte[] secretDate = hmac256(("TC3" + properties.getApiSecret()).getBytes(StandardCharsets.UTF_8), date);
byte[] secretService = hmac256(secretDate, "sms");
byte[] secretSigning = hmac256(secretService, "tc3_request");
String signature = DatatypeConverter.printHexBinary(hmac256(secretSigning, stringToSign)).toLowerCase();
// ************* 步骤 4拼接 Authorization *************
String authorization = "TC3-HMAC-SHA256" + " " + "Credential=" + getApiKey() + "/" + credentialScope + ", "
+ "SignedHeaders=" + signedHeaders + ", " + "Signature=" + signature;
// ************* 步骤 5构造HttpRequest 并执行request请求获得response *************
Map<String, String> headers = new HashMap<>(); Map<String, String> headers = new HashMap<>();
headers.put("Authorization", authorization);
headers.put("Content-Type", "application/json; charset=utf-8"); headers.put("Content-Type", "application/json; charset=utf-8");
headers.put("Host", host); headers.put("Host", HOST);
headers.put("X-TC-Action", action); headers.put("X-TC-Action", action);
headers.put("X-TC-Timestamp", timestamp); Date now = new Date();
String nowStr = FastDateFormat.getInstance("yyyy-MM-dd", TimeZone.getTimeZone("UTC")).format(now);
headers.put("X-TC-Timestamp", String.valueOf(now.getTime() / 1000));
headers.put("X-TC-Version", VERSION); headers.put("X-TC-Version", VERSION);
headers.put("X-TC-Region", REGION); headers.put("X-TC-Region", REGION);
String responseBody = HttpUtils.post("https://" + host, headers, JSONUtil.toJsonStr(body)); // 1.2 构建签名 Header
String canonicalQueryString = "";
String canonicalHeaders = "content-type:application/json; charset=utf-8\n"
+ "host:" + HOST + "\n" + "x-tc-action:" + action.toLowerCase() + "\n";
String signedHeaders = "content-type;host;x-tc-action";
String canonicalRequest = "POST" + "\n" + "/" + "\n" + canonicalQueryString + "\n" + canonicalHeaders + "\n"
+ signedHeaders + "\n" + sha256Hex(JSONUtil.toJsonStr(body));
String credentialScope = nowStr + "/" + "sms" + "/" + "tc3_request";
String stringToSign = "TC3-HMAC-SHA256" + "\n" + now.getTime() / 1000 + "\n" + credentialScope + "\n" +
sha256Hex(canonicalRequest);
byte[] secretService = hmac256(hmac256(("TC3" + properties.getApiSecret()).getBytes(StandardCharsets.UTF_8), nowStr), "sms");
String signature = HexUtil.encodeHexStr(hmac256(hmac256(secretService, "tc3_request"), stringToSign));
headers.put("Authorization", "TC3-HMAC-SHA256" + " " + "Credential=" + getApiKey() + "/" + credentialScope + ", "
+ "SignedHeaders=" + signedHeaders + ", " + "Signature=" + signature);
// 2. 发起请求
String responseBody = HttpUtils.post("https://" + HOST, headers, JSONUtil.toJsonStr(body));
return JSONUtil.parseObj(responseBody); return JSONUtil.parseObj(responseBody);
} }
// TODO @scholar使用 hutool 简化下 private static byte[] hmac256(byte[] key, String msg) {
private static byte[] hmac256(byte[] key, String msg) throws Exception { return DigestUtil.hmac(HmacAlgorithm.HmacSHA256, key).digest(msg);
Mac mac = Mac.getInstance("HmacSHA256");
SecretKeySpec secretKeySpec = new SecretKeySpec(key, mac.getAlgorithm());
mac.init(secretKeySpec);
return mac.doFinal(msg.getBytes(StandardCharsets.UTF_8));
} }
} }

View File

@ -18,6 +18,7 @@ public enum SmsChannelEnum {
ALIYUN("ALIYUN", "阿里云"), ALIYUN("ALIYUN", "阿里云"),
TENCENT("TENCENT", "腾讯云"), TENCENT("TENCENT", "腾讯云"),
HUAWEI("HUAWEI", "华为云"), HUAWEI("HUAWEI", "华为云"),
QINIU("QINIU", "七牛云"),
; ;
/** /**
@ -34,3 +35,4 @@ public enum SmsChannelEnum {
} }
} }

View File

@ -56,7 +56,7 @@ public class OAuth2TokenServiceImpl implements OAuth2TokenService {
private AdminUserService adminUserService; private AdminUserService adminUserService;
@Override @Override
@Transactional @Transactional(rollbackFor = Exception.class)
public OAuth2AccessTokenDO createAccessToken(Long userId, Integer userType, String clientId, List<String> scopes) { public OAuth2AccessTokenDO createAccessToken(Long userId, Integer userType, String clientId, List<String> scopes) {
OAuth2ClientDO clientDO = oauth2ClientService.validOAuthClientFromCache(clientId); OAuth2ClientDO clientDO = oauth2ClientService.validOAuthClientFromCache(clientId);
// 创建刷新令牌 // 创建刷新令牌
@ -66,6 +66,7 @@ public class OAuth2TokenServiceImpl implements OAuth2TokenService {
} }
@Override @Override
@Transactional(rollbackFor = Exception.class)
public OAuth2AccessTokenDO refreshAccessToken(String refreshToken, String clientId) { public OAuth2AccessTokenDO refreshAccessToken(String refreshToken, String clientId) {
// 查询访问令牌 // 查询访问令牌
OAuth2RefreshTokenDO refreshTokenDO = oauth2RefreshTokenMapper.selectByRefreshToken(refreshToken); OAuth2RefreshTokenDO refreshTokenDO = oauth2RefreshTokenMapper.selectByRefreshToken(refreshToken);
@ -82,7 +83,7 @@ public class OAuth2TokenServiceImpl implements OAuth2TokenService {
// 移除相关的访问令牌 // 移除相关的访问令牌
List<OAuth2AccessTokenDO> accessTokenDOs = oauth2AccessTokenMapper.selectListByRefreshToken(refreshToken); List<OAuth2AccessTokenDO> accessTokenDOs = oauth2AccessTokenMapper.selectListByRefreshToken(refreshToken);
if (CollUtil.isNotEmpty(accessTokenDOs)) { if (CollUtil.isNotEmpty(accessTokenDOs)) {
oauth2AccessTokenMapper.deleteBatchIds(convertSet(accessTokenDOs, OAuth2AccessTokenDO::getId)); oauth2AccessTokenMapper.deleteByIds(convertSet(accessTokenDOs, OAuth2AccessTokenDO::getId));
oauth2AccessTokenRedisDAO.deleteList(convertSet(accessTokenDOs, OAuth2AccessTokenDO::getAccessToken)); oauth2AccessTokenRedisDAO.deleteList(convertSet(accessTokenDOs, OAuth2AccessTokenDO::getAccessToken));
} }
@ -126,6 +127,7 @@ public class OAuth2TokenServiceImpl implements OAuth2TokenService {
} }
@Override @Override
@Transactional(rollbackFor = Exception.class)
public OAuth2AccessTokenDO removeAccessToken(String accessToken) { public OAuth2AccessTokenDO removeAccessToken(String accessToken) {
// 删除访问令牌 // 删除访问令牌
OAuth2AccessTokenDO accessTokenDO = oauth2AccessTokenMapper.selectByAccessToken(accessToken); OAuth2AccessTokenDO accessTokenDO = oauth2AccessTokenMapper.selectByAccessToken(accessToken);

View File

@ -1,27 +1,21 @@
package cn.iocoder.yudao.module.system.service.sms; package cn.iocoder.yudao.module.system.service.sms;
import cn.hutool.core.util.StrUtil;
import cn.iocoder.yudao.framework.common.pojo.PageResult; import cn.iocoder.yudao.framework.common.pojo.PageResult;
import cn.iocoder.yudao.framework.common.util.object.BeanUtils; import cn.iocoder.yudao.framework.common.util.object.BeanUtils;
import cn.iocoder.yudao.module.system.framework.sms.core.client.SmsClient;
import cn.iocoder.yudao.module.system.framework.sms.core.client.SmsClientFactory;
import cn.iocoder.yudao.module.system.framework.sms.core.property.SmsChannelProperties;
import cn.iocoder.yudao.module.system.controller.admin.sms.vo.channel.SmsChannelPageReqVO; import cn.iocoder.yudao.module.system.controller.admin.sms.vo.channel.SmsChannelPageReqVO;
import cn.iocoder.yudao.module.system.controller.admin.sms.vo.channel.SmsChannelSaveReqVO; import cn.iocoder.yudao.module.system.controller.admin.sms.vo.channel.SmsChannelSaveReqVO;
import cn.iocoder.yudao.module.system.dal.dataobject.sms.SmsChannelDO; import cn.iocoder.yudao.module.system.dal.dataobject.sms.SmsChannelDO;
import cn.iocoder.yudao.module.system.dal.mysql.sms.SmsChannelMapper; import cn.iocoder.yudao.module.system.dal.mysql.sms.SmsChannelMapper;
import com.google.common.cache.CacheLoader; import cn.iocoder.yudao.module.system.framework.sms.core.client.SmsClient;
import com.google.common.cache.LoadingCache; import cn.iocoder.yudao.module.system.framework.sms.core.client.SmsClientFactory;
import lombok.Getter; import cn.iocoder.yudao.module.system.framework.sms.core.property.SmsChannelProperties;
import jakarta.annotation.Resource;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
import jakarta.annotation.Resource;
import java.time.Duration;
import java.util.List; import java.util.List;
import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception; import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception;
import static cn.iocoder.yudao.framework.common.util.cache.CacheUtils.buildAsyncReloadingCache;
import static cn.iocoder.yudao.module.system.enums.ErrorCodeConstants.SMS_CHANNEL_HAS_CHILDREN; import static cn.iocoder.yudao.module.system.enums.ErrorCodeConstants.SMS_CHANNEL_HAS_CHILDREN;
import static cn.iocoder.yudao.module.system.enums.ErrorCodeConstants.SMS_CHANNEL_NOT_EXISTS; import static cn.iocoder.yudao.module.system.enums.ErrorCodeConstants.SMS_CHANNEL_NOT_EXISTS;
@ -34,46 +28,6 @@ import static cn.iocoder.yudao.module.system.enums.ErrorCodeConstants.SMS_CHANNE
@Slf4j @Slf4j
public class SmsChannelServiceImpl implements SmsChannelService { public class SmsChannelServiceImpl implements SmsChannelService {
/**
* {@link SmsClient} 缓存通过它异步刷新 smsClientFactory
*/
@Getter
private final LoadingCache<Long, SmsClient> idClientCache = buildAsyncReloadingCache(Duration.ofSeconds(10L),
new CacheLoader<Long, SmsClient>() {
@Override
public SmsClient load(Long id) {
// 查询然后尝试刷新
SmsChannelDO channel = smsChannelMapper.selectById(id);
if (channel != null) {
SmsChannelProperties properties = BeanUtils.toBean(channel, SmsChannelProperties.class);
smsClientFactory.createOrUpdateSmsClient(properties);
}
return smsClientFactory.getSmsClient(id);
}
});
/**
* {@link SmsClient} 缓存通过它异步刷新 smsClientFactory
*/
@Getter
private final LoadingCache<String, SmsClient> codeClientCache = buildAsyncReloadingCache(Duration.ofSeconds(60L),
new CacheLoader<String, SmsClient>() {
@Override
public SmsClient load(String code) {
// 查询然后尝试刷新
SmsChannelDO channel = smsChannelMapper.selectByCode(code);
if (channel != null) {
SmsChannelProperties properties = BeanUtils.toBean(channel, SmsChannelProperties.class);
smsClientFactory.createOrUpdateSmsClient(properties);
}
return smsClientFactory.getSmsClient(code);
}
});
@Resource @Resource
private SmsClientFactory smsClientFactory; private SmsClientFactory smsClientFactory;
@ -93,41 +47,22 @@ public class SmsChannelServiceImpl implements SmsChannelService {
@Override @Override
public void updateSmsChannel(SmsChannelSaveReqVO updateReqVO) { public void updateSmsChannel(SmsChannelSaveReqVO updateReqVO) {
// 校验存在 // 校验存在
SmsChannelDO channel = validateSmsChannelExists(updateReqVO.getId()); validateSmsChannelExists(updateReqVO.getId());
// 更新 // 更新
SmsChannelDO updateObj = BeanUtils.toBean(updateReqVO, SmsChannelDO.class); SmsChannelDO updateObj = BeanUtils.toBean(updateReqVO, SmsChannelDO.class);
smsChannelMapper.updateById(updateObj); smsChannelMapper.updateById(updateObj);
// 清空缓存
clearCache(updateReqVO.getId(), channel.getCode());
} }
@Override @Override
public void deleteSmsChannel(Long id) { public void deleteSmsChannel(Long id) {
// 校验存在 // 校验存在
SmsChannelDO channel = validateSmsChannelExists(id); validateSmsChannelExists(id);
// 校验是否有在使用该账号的模版 // 校验是否有在使用该账号的模版
if (smsTemplateService.getSmsTemplateCountByChannelId(id) > 0) { if (smsTemplateService.getSmsTemplateCountByChannelId(id) > 0) {
throw exception(SMS_CHANNEL_HAS_CHILDREN); throw exception(SMS_CHANNEL_HAS_CHILDREN);
} }
// 删除 // 删除
smsChannelMapper.deleteById(id); smsChannelMapper.deleteById(id);
// 清空缓存
clearCache(id, channel.getCode());
}
/**
* 清空指定渠道编号的缓存
*
* @param id 渠道编号
* @param code 渠道编码
*/
private void clearCache(Long id, String code) {
idClientCache.invalidate(id);
if (StrUtil.isNotEmpty(code)) {
codeClientCache.invalidate(code);
}
} }
private SmsChannelDO validateSmsChannelExists(Long id) { private SmsChannelDO validateSmsChannelExists(Long id) {
@ -155,12 +90,14 @@ public class SmsChannelServiceImpl implements SmsChannelService {
@Override @Override
public SmsClient getSmsClient(Long id) { public SmsClient getSmsClient(Long id) {
return idClientCache.getUnchecked(id); SmsChannelDO channel = smsChannelMapper.selectById(id);
SmsChannelProperties properties = BeanUtils.toBean(channel, SmsChannelProperties.class);
return smsClientFactory.createOrUpdateSmsClient(properties);
} }
@Override @Override
public SmsClient getSmsClient(String code) { public SmsClient getSmsClient(String code) {
return codeClientCache.getUnchecked(code); return smsClientFactory.getSmsClient(code);
} }
} }

View File

@ -0,0 +1,127 @@
package cn.iocoder.yudao.module.system.framework.sms.core.client.impl;
import cn.iocoder.yudao.framework.common.core.KeyValue;
import cn.iocoder.yudao.framework.common.util.http.HttpUtils;
import cn.iocoder.yudao.framework.test.core.ut.BaseMockitoUnitTest;
import cn.iocoder.yudao.module.system.framework.sms.core.client.dto.SmsReceiveRespDTO;
import cn.iocoder.yudao.module.system.framework.sms.core.client.dto.SmsSendRespDTO;
import cn.iocoder.yudao.module.system.framework.sms.core.property.SmsChannelProperties;
import com.google.common.collect.Lists;
import org.junit.jupiter.api.Test;
import org.mockito.InjectMocks;
import org.mockito.MockedStatic;
import java.time.LocalDateTime;
import java.util.List;
import static cn.iocoder.yudao.framework.test.core.util.RandomUtils.*;
import static org.junit.jupiter.api.Assertions.*;
import static org.mockito.ArgumentMatchers.*;
import static org.mockito.ArgumentMatchers.anyString;
import static org.mockito.Mockito.mockStatic;
/**
* {@link HuaweiSmsClient} 的单元测试
*
* @author scholar
*/
public class HuaweiSmsClientTest extends BaseMockitoUnitTest {
private final SmsChannelProperties properties = new SmsChannelProperties()
.setApiKey(randomString() + " " + randomString()) // 随机一个 apiKey避免构建报错
.setApiSecret(randomString()) // 随机一个 apiSecret避免构建报错
.setSignature("芋道源码");
@InjectMocks
private HuaweiSmsClient smsClient = new HuaweiSmsClient(properties);
@Test
public void testDoSendSms_success() throws Throwable {
try (MockedStatic<HttpUtils> httpUtilsMockedStatic = mockStatic(HttpUtils.class)) {
// 准备参数
Long sendLogId = randomLongId();
String mobile = randomString();
String apiTemplateId = randomString() + " " + randomString();
List<KeyValue<String, Object>> templateParams = Lists.newArrayList(
new KeyValue<>("1", 1234), new KeyValue<>("2", "login"));
// mock 方法
httpUtilsMockedStatic.when(() -> HttpUtils.post(anyString(), anyMap(), anyString()))
.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");
// 调用
SmsSendRespDTO result = smsClient.sendSms(sendLogId, mobile,
apiTemplateId, templateParams);
// 断言
assertTrue(result.getSuccess());
assertEquals("d6e3cdd0-522b-4692-8304-a07553cdf591_8539659", result.getSerialNo());
assertEquals("000000", result.getApiCode());
}
}
@Test
public void testDoSendSms_fail_01() throws Throwable {
try (MockedStatic<HttpUtils> httpUtilsMockedStatic = mockStatic(HttpUtils.class)) {
// 准备参数
Long sendLogId = randomLongId();
String mobile = randomString();
String apiTemplateId = randomString() + " " + randomString();
List<KeyValue<String, Object>> templateParams = Lists.newArrayList(
new KeyValue<>("1", 1234), new KeyValue<>("2", "login"));
// mock 方法
httpUtilsMockedStatic.when(() -> HttpUtils.post(anyString(), anyMap(), anyString()))
.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.\"}");
// 调用
SmsSendRespDTO result = smsClient.sendSms(sendLogId, mobile,
apiTemplateId, templateParams);
// 断言
assertFalse(result.getSuccess());
assertEquals("06e4b966-ad87-479f-8b74-f57fb7aafb60_304613461", result.getSerialNo());
assertEquals("E200033", result.getApiCode());
}
}
@Test
public void testDoSendSms_fail_02() throws Throwable {
try (MockedStatic<HttpUtils> httpUtilsMockedStatic = mockStatic(HttpUtils.class)) {
// 准备参数
Long sendLogId = randomLongId();
String mobile = randomString();
String apiTemplateId = randomString() + " " + randomString();
List<KeyValue<String, Object>> templateParams = Lists.newArrayList(
new KeyValue<>("1", 1234), new KeyValue<>("2", "login"));
// mock 方法
httpUtilsMockedStatic.when(() -> HttpUtils.post(anyString(), anyMap(), anyString()))
.thenReturn("{\"code\":\"E000102\",\"description\":\"Invalid app_key.\"}");
// 调用
SmsSendRespDTO result = smsClient.sendSms(sendLogId, mobile,
apiTemplateId, templateParams);
// 断言
assertFalse(result.getSuccess());
assertEquals("E000102", result.getApiCode());
assertEquals("Invalid app_key.", result.getApiMsg());
}
}
@Test
public void testParseSmsReceiveStatus() {
// 准备参数
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";
// 调用
List<SmsReceiveRespDTO> statuses = smsClient.parseSmsReceiveStatus(text);
// 断言
assertEquals(1, statuses.size());
SmsReceiveRespDTO status = statuses.get(0);
assertTrue(status.getSuccess());
assertEquals("DELIVRD", status.getErrorCode());
assertEquals(LocalDateTime.of(2024, 8, 15, 3, 0, 34), status.getReceiveTime());
assertEquals("70207ed7-1d02-41b0-8537-bb25fd1c2364_143684459", status.getSerialNo());
}
}

View File

@ -0,0 +1,131 @@
package cn.iocoder.yudao.module.system.framework.sms.core.client.impl;
import cn.iocoder.yudao.framework.common.core.KeyValue;
import cn.iocoder.yudao.framework.common.util.http.HttpUtils;
import cn.iocoder.yudao.framework.test.core.ut.BaseMockitoUnitTest;
import cn.iocoder.yudao.module.system.framework.sms.core.client.dto.SmsReceiveRespDTO;
import cn.iocoder.yudao.module.system.framework.sms.core.client.dto.SmsSendRespDTO;
import cn.iocoder.yudao.module.system.framework.sms.core.client.dto.SmsTemplateRespDTO;
import cn.iocoder.yudao.module.system.framework.sms.core.enums.SmsTemplateAuditStatusEnum;
import cn.iocoder.yudao.module.system.framework.sms.core.property.SmsChannelProperties;
import com.google.common.collect.Lists;
import org.junit.jupiter.api.Test;
import org.mockito.InjectMocks;
import org.mockito.MockedStatic;
import java.time.LocalDateTime;
import java.util.List;
import static cn.iocoder.yudao.framework.test.core.util.RandomUtils.*;
import static org.junit.jupiter.api.Assertions.*;
import static org.mockito.ArgumentMatchers.*;
import static org.mockito.ArgumentMatchers.anyString;
import static org.mockito.Mockito.mockStatic;
/**
* {@link QiniuSmsClient} 的单元测试
*
* @author scholar
*/
public class QiniuSmsClientTest extends BaseMockitoUnitTest {
private final SmsChannelProperties properties = new SmsChannelProperties()
.setApiKey(randomString())// 随机一个 apiKey避免构建报错
.setApiSecret(randomString()) // 随机一个 apiSecret避免构建报错
.setSignature("芋道源码");
@InjectMocks
private QiniuSmsClient smsClient = new QiniuSmsClient(properties);
@Test
public void testDoSendSms_success() throws Throwable {
try (MockedStatic<HttpUtils> httpUtilsMockedStatic = mockStatic(HttpUtils.class)) {
// 准备参数
Long sendLogId = randomLongId();
String mobile = randomString();
String apiTemplateId = randomString() + " " + randomString();
List<KeyValue<String, Object>> templateParams = Lists.newArrayList(
new KeyValue<>("1", 1234), new KeyValue<>("2", "login"));
// mock 方法
httpUtilsMockedStatic.when(() -> HttpUtils.post(anyString(), anyMap(), anyString()))
.thenReturn("{\"message_id\":\"17245678901\"}");
// 调用
SmsSendRespDTO result = smsClient.sendSms(sendLogId, mobile,
apiTemplateId, templateParams);
// 断言
assertTrue(result.getSuccess());
assertEquals("17245678901", result.getSerialNo());
}
}
@Test
public void testDoSendSms_fail() throws Throwable {
try (MockedStatic<HttpUtils> httpUtilsMockedStatic = mockStatic(HttpUtils.class)) {
// 准备参数
Long sendLogId = randomLongId();
String mobile = randomString();
String apiTemplateId = randomString() + " " + randomString();
List<KeyValue<String, Object>> templateParams = Lists.newArrayList(
new KeyValue<>("1", 1234), new KeyValue<>("2", "login"));
// mock 方法
httpUtilsMockedStatic.when(() -> HttpUtils.post(anyString(), anyMap(), anyString()))
.thenReturn("{\"error\":\"BadToken\",\"message\":\"Your authorization token is invalid\",\"request_id\":\"etziWcJFo1C8Ne8X\"}");
// 调用
SmsSendRespDTO result = smsClient.sendSms(sendLogId, mobile,
apiTemplateId, templateParams);
// 断言
assertFalse(result.getSuccess());
assertEquals("BadToken", result.getApiCode());
assertEquals("Your authorization token is invalid", result.getApiMsg());
assertEquals("etziWcJFo1C8Ne8X", result.getApiRequestId());
}
}
@Test
public void testGetSmsTemplate() throws Throwable {
try (MockedStatic<HttpUtils> httpUtilsMockedStatic = mockStatic(HttpUtils.class)) {
// 准备参数
String apiTemplateId = randomString();
// mock 方法
httpUtilsMockedStatic.when(() -> HttpUtils.get(anyString(), anyMap()))
.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}");
// 调用
SmsTemplateRespDTO result = smsClient.getSmsTemplate(apiTemplateId);
// 断言
assertEquals("1826184073773596672", result.getId());
assertEquals("您的验证码为:${code}", result.getContent());
assertEquals(SmsTemplateAuditStatusEnum.SUCCESS.getStatus(), result.getAuditStatus());
assertEquals("", result.getAuditReason());
}
}
@Test
public void testParseSmsReceiveStatus() {
// 准备参数
String text = "{\"items\":[{\"mobile\":\"18881234567\",\"message_id\":\"10135515063508004167\",\"status\":\"DELIVRD\",\"delivrd_at\":1724591666,\"error\":\"DELIVRD\",\"seq\":\"123\"}]}";
// 调用
List<SmsReceiveRespDTO> statuses = smsClient.parseSmsReceiveStatus(text);
// 断言
assertEquals(1, statuses.size());
SmsReceiveRespDTO status = statuses.get(0);
assertTrue(status.getSuccess());
assertEquals("DELIVRD", status.getErrorMsg());
assertEquals(LocalDateTime.of(2024, 8, 25, 21, 14, 26), status.getReceiveTime());
assertEquals("18881234567", status.getMobile());
assertEquals("10135515063508004167", status.getSerialNo());
assertEquals(123, status.getLogId());
}
@Test
public void testConvertSmsTemplateAuditStatus() {
assertEquals(SmsTemplateAuditStatusEnum.SUCCESS.getStatus(),
smsClient.convertSmsTemplateAuditStatus("passed"));
assertEquals(SmsTemplateAuditStatusEnum.CHECKING.getStatus(),
smsClient.convertSmsTemplateAuditStatus("reviewing"));
assertEquals(SmsTemplateAuditStatusEnum.FAIL.getStatus(),
smsClient.convertSmsTemplateAuditStatus("rejected"));
assertThrows(IllegalArgumentException.class, () -> smsClient.convertSmsTemplateAuditStatus("unknown"),
"未知审核状态(3)");
}
}

View File

@ -2,7 +2,7 @@ package cn.iocoder.yudao.module.system.framework.sms.core.client.impl;
import cn.hutool.core.collection.ListUtil; import cn.hutool.core.collection.ListUtil;
import cn.iocoder.yudao.framework.common.core.KeyValue; import cn.iocoder.yudao.framework.common.core.KeyValue;
import cn.iocoder.yudao.module.system.framework.sms.core.client.dto.SmsReceiveRespDTO; import cn.iocoder.yudao.module.system.framework.sms.core.client.SmsClient;
import cn.iocoder.yudao.module.system.framework.sms.core.client.dto.SmsSendRespDTO; import cn.iocoder.yudao.module.system.framework.sms.core.client.dto.SmsSendRespDTO;
import cn.iocoder.yudao.module.system.framework.sms.core.client.dto.SmsTemplateRespDTO; import cn.iocoder.yudao.module.system.framework.sms.core.client.dto.SmsTemplateRespDTO;
import cn.iocoder.yudao.module.system.framework.sms.core.property.SmsChannelProperties; import cn.iocoder.yudao.module.system.framework.sms.core.property.SmsChannelProperties;
@ -12,7 +12,7 @@ import org.junit.jupiter.api.Test;
import java.util.List; import java.util.List;
/** /**
* 各种 {@link SmsClientTests 集成测试 * 各种 {@link SmsClient} 集成测试
* *
* @author 芋道源码 * @author 芋道源码
*/ */
@ -24,8 +24,8 @@ public class SmsClientTests {
@Disabled @Disabled
public void testAliyunSmsClient_getSmsTemplate() throws Throwable { public void testAliyunSmsClient_getSmsTemplate() throws Throwable {
SmsChannelProperties properties = new SmsChannelProperties() SmsChannelProperties properties = new SmsChannelProperties()
.setApiKey("LTAI5tAicJAxaSFiZuGGeXHR") .setApiKey(System.getenv("SMS_ALIYUN_ACCESS_KEY"))
.setApiSecret("Fdr9vadxnDvS6GJU0W1tijQ0VmLhYz"); .setApiSecret(System.getenv("SMS_ALIYUN_SECRET_KEY"));
AliyunSmsClient client = new AliyunSmsClient(properties); AliyunSmsClient client = new AliyunSmsClient(properties);
// 准备参数 // 准备参数
String apiTemplateId = "SMS_207945135"; String apiTemplateId = "SMS_207945135";
@ -39,9 +39,9 @@ public class SmsClientTests {
@Disabled @Disabled
public void testAliyunSmsClient_sendSms() throws Throwable { public void testAliyunSmsClient_sendSms() throws Throwable {
SmsChannelProperties properties = new SmsChannelProperties() SmsChannelProperties properties = new SmsChannelProperties()
.setApiKey("LTAI5tAicJAxaSFiZuGGeXHR") .setApiKey(System.getenv("SMS_ALIYUN_ACCESS_KEY"))
.setApiSecret("Fdr9vadxnDvS6GJU0W1tijQ0VmLhYz") .setApiSecret(System.getenv("SMS_ALIYUN_SECRET_KEY"))
.setSignature("runpu"); .setSignature("Ballcat");
AliyunSmsClient client = new AliyunSmsClient(properties); AliyunSmsClient client = new AliyunSmsClient(properties);
// 准备参数 // 准备参数
Long sendLogId = System.currentTimeMillis(); Long sendLogId = System.currentTimeMillis();
@ -53,49 +53,21 @@ public class SmsClientTests {
System.out.println(sendRespDTO); System.out.println(sendRespDTO);
} }
@Test
@Disabled
public void testAliyunSmsClient_parseSmsReceiveStatus() {
SmsChannelProperties properties = new SmsChannelProperties()
.setApiKey("LTAI5tAicJAxaSFiZuGGeXHR")
.setApiSecret("Fdr9vadxnDvS6GJU0W1tijQ0VmLhYz");
AliyunSmsClient client = new AliyunSmsClient(properties);
// 准备参数
String text = "[\n" +
" {\n" +
" \"phone_number\" : \"13900000001\",\n" +
" \"send_time\" : \"2017-01-01 11:12:13\",\n" +
" \"report_time\" : \"2017-02-02 22:23:24\",\n" +
" \"success\" : true,\n" +
" \"err_code\" : \"DELIVERED\",\n" +
" \"err_msg\" : \"用户接收成功\",\n" +
" \"sms_size\" : \"1\",\n" +
" \"biz_id\" : \"12345\",\n" +
" \"out_id\" : \"67890\"\n" +
" }\n" +
"]";
// mock 方法
// 调用
List<SmsReceiveRespDTO> statuses = client.parseSmsReceiveStatus(text);
// 打印结果
System.out.println(statuses);
}
// ========== 腾讯云 ========== // ========== 腾讯云 ==========
@Test @Test
@Disabled @Disabled
public void testTencentSmsClient_sendSms() throws Throwable { public void testTencentSmsClient_sendSms() throws Throwable {
String sdkAppId = "1400500458";
SmsChannelProperties properties = new SmsChannelProperties() SmsChannelProperties properties = new SmsChannelProperties()
.setApiKey("LTAI5tAicJAxaSFiZuGGeXHR 1428926523") .setApiKey(System.getenv("SMS_TENCENT_ACCESS_KEY") + " " + sdkAppId)
.setApiSecret("Fdr9vadxnDvS6GJU0W1tijQ0VmLhYz") .setApiSecret(System.getenv("SMS_TENCENT_SECRET_KEY"))
.setSignature("芋道源码"); .setSignature("芋道源码");
TencentSmsClient client = new TencentSmsClient(properties); TencentSmsClient client = new TencentSmsClient(properties);
// 准备参数 // 准备参数
Long sendLogId = System.currentTimeMillis(); Long sendLogId = System.currentTimeMillis();
String mobile = "15601691323"; String mobile = "15601691323";
String apiTemplateId = "2136358"; String apiTemplateId = "358212";
// 调用 // 调用
SmsSendRespDTO sendRespDTO = client.sendSms(sendLogId, mobile, apiTemplateId, ListUtil.of(new KeyValue<>("code", "1024"))); SmsSendRespDTO sendRespDTO = client.sendSms(sendLogId, mobile, apiTemplateId, ListUtil.of(new KeyValue<>("code", "1024")));
// 打印结果 // 打印结果
@ -105,13 +77,14 @@ public class SmsClientTests {
@Test @Test
@Disabled @Disabled
public void testTencentSmsClient_getSmsTemplate() throws Throwable { public void testTencentSmsClient_getSmsTemplate() throws Throwable {
String sdkAppId = "1400500458";
SmsChannelProperties properties = new SmsChannelProperties() SmsChannelProperties properties = new SmsChannelProperties()
.setApiKey("LTAI5tAicJAxaSFiZuGGeXHR 1428926523") .setApiKey(System.getenv("SMS_TENCENT_ACCESS_KEY") + " " + sdkAppId)
.setApiSecret("Fdr9vadxnDvS6GJU0W1tijQ0VmLhYz") .setApiSecret(System.getenv("SMS_TENCENT_SECRET_KEY"))
.setSignature("芋道源码"); .setSignature("芋道源码");
TencentSmsClient client = new TencentSmsClient(properties); TencentSmsClient client = new TencentSmsClient(properties);
// 准备参数 // 准备参数
String apiTemplateId = "2136358"; String apiTemplateId = "358212";
// 调用 // 调用
SmsTemplateRespDTO template = client.getSmsTemplate(apiTemplateId); SmsTemplateRespDTO template = client.getSmsTemplate(apiTemplateId);
// 打印结果 // 打印结果
@ -123,15 +96,16 @@ public class SmsClientTests {
@Test @Test
@Disabled @Disabled
public void testHuaweiSmsClient_sendSms() throws Throwable { public void testHuaweiSmsClient_sendSms() throws Throwable {
String sender = "x8824060312575";
SmsChannelProperties properties = new SmsChannelProperties() SmsChannelProperties properties = new SmsChannelProperties()
.setApiKey("123") .setApiKey(System.getenv("SMS_HUAWEI_ACCESS_KEY") + " " + sender)
.setApiSecret("456") .setApiSecret(System.getenv("SMS_HUAWEI_SECRET_KEY"))
.setSignature("runpu"); .setSignature("runpu");
HuaweiSmsClient client = new HuaweiSmsClient(properties); HuaweiSmsClient client = new HuaweiSmsClient(properties);
// 准备参数 // 准备参数
Long sendLogId = System.currentTimeMillis(); Long sendLogId = System.currentTimeMillis();
String mobile = "15601691323"; String mobile = "17321315478";
String apiTemplateId = "xx test01"; String apiTemplateId = "3644cdab863546a3b718d488659a99ef";
List<KeyValue<String, Object>> templateParams = ListUtil.of(new KeyValue<>("code", "1024")); List<KeyValue<String, Object>> templateParams = ListUtil.of(new KeyValue<>("code", "1024"));
// 调用 // 调用
SmsSendRespDTO smsSendRespDTO = client.sendSms(sendLogId, mobile, apiTemplateId, templateParams); SmsSendRespDTO smsSendRespDTO = client.sendSms(sendLogId, mobile, apiTemplateId, templateParams);
@ -139,5 +113,39 @@ public class SmsClientTests {
System.out.println(smsSendRespDTO); System.out.println(smsSendRespDTO);
} }
// ========== 七牛云 ==========
@Test
@Disabled
public void testQiniuSmsClient_sendSms() throws Throwable {
SmsChannelProperties properties = new SmsChannelProperties()
.setApiKey("SMS_QINIU_ACCESS_KEY")
.setApiSecret("SMS_QINIU_SECRET_KEY");
QiniuSmsClient client = new QiniuSmsClient(properties);
// 准备参数
Long sendLogId = System.currentTimeMillis();
String mobile = "17321315478";
String apiTemplateId = "3644cdab863546a3b718d488659a99ef";
List<KeyValue<String, Object>> templateParams = ListUtil.of(new KeyValue<>("code", "1122"));
// 调用
SmsSendRespDTO smsSendRespDTO = client.sendSms(sendLogId, mobile, apiTemplateId, templateParams);
// 打印结果
System.out.println(smsSendRespDTO);
}
@Test
@Disabled
public void testQiniuSmsClient_getSmsTemplate() throws Throwable {
SmsChannelProperties properties = new SmsChannelProperties()
.setApiKey("SMS_QINIU_ACCESS_KEY")
.setApiSecret("SMS_QINIU_SECRET_KEY");
QiniuSmsClient client = new QiniuSmsClient(properties);
// 准备参数
String apiTemplateId = "3644cdab863546a3b718d488659a99ef";
// 调用
SmsTemplateRespDTO template = client.getSmsTemplate(apiTemplateId);
// 打印结果
System.out.println(template);
}
} }

View File

@ -78,7 +78,7 @@ public class TencentSmsClientTest extends BaseMockitoUnitTest {
} }
@Test @Test
public void testDoSendSms_fail() throws Throwable { public void testDoSendSms_fail_01() throws Throwable {
try (MockedStatic<HttpUtils> httpUtilsMockedStatic = mockStatic(HttpUtils.class)) { try (MockedStatic<HttpUtils> httpUtilsMockedStatic = mockStatic(HttpUtils.class)) {
// 准备参数 // 准备参数
Long sendLogId = randomLongId(); Long sendLogId = randomLongId();
@ -117,6 +117,31 @@ public class TencentSmsClientTest extends BaseMockitoUnitTest {
} }
} }
@Test
public void testDoSendSms_fail_02() throws Throwable {
try (MockedStatic<HttpUtils> httpUtilsMockedStatic = mockStatic(HttpUtils.class)) {
// 准备参数
Long sendLogId = randomLongId();
String mobile = randomString();
String apiTemplateId = randomString();
List<KeyValue<String, Object>> templateParams = Lists.newArrayList(
new KeyValue<>("1", 1234), new KeyValue<>("2", "login"));
// mock 方法
httpUtilsMockedStatic.when(() -> HttpUtils.post(anyString(), anyMap(), anyString()))
.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\"}}");
// 调用
SmsSendRespDTO result = smsClient.sendSms(sendLogId, mobile,
apiTemplateId, templateParams);
// 断言
assertFalse(result.getSuccess());
assertEquals("2a88f82a-261c-4ac6-9fa9-c7d01aaa486a", result.getApiRequestId());
assertEquals("AuthFailure.SecretIdNotFound", result.getApiCode());
assertEquals("The SecretId is not found, please ensure that your SecretId is correct.", result.getApiMsg());
}
}
@Test @Test
public void testParseSmsReceiveStatus() { public void testParseSmsReceiveStatus() {
// 准备参数 // 准备参数

View File

@ -57,9 +57,6 @@ public class SmsChannelServiceTest extends BaseDbUnitTest {
// 校验记录的属性是否正确 // 校验记录的属性是否正确
SmsChannelDO smsChannel = smsChannelMapper.selectById(smsChannelId); SmsChannelDO smsChannel = smsChannelMapper.selectById(smsChannelId);
assertPojoEquals(reqVO, smsChannel, "id"); assertPojoEquals(reqVO, smsChannel, "id");
// 断言 cache
assertNull(smsChannelService.getIdClientCache().getIfPresent(smsChannel.getId()));
assertNull(smsChannelService.getCodeClientCache().getIfPresent(smsChannel.getCode()));
} }
@Test @Test
@ -79,9 +76,6 @@ public class SmsChannelServiceTest extends BaseDbUnitTest {
// 校验是否更新正确 // 校验是否更新正确
SmsChannelDO smsChannel = smsChannelMapper.selectById(reqVO.getId()); // 获取最新的 SmsChannelDO smsChannel = smsChannelMapper.selectById(reqVO.getId()); // 获取最新的
assertPojoEquals(reqVO, smsChannel); assertPojoEquals(reqVO, smsChannel);
// 断言 cache
assertNull(smsChannelService.getIdClientCache().getIfPresent(smsChannel.getId()));
assertNull(smsChannelService.getCodeClientCache().getIfPresent(smsChannel.getCode()));
} }
@Test @Test
@ -105,9 +99,6 @@ public class SmsChannelServiceTest extends BaseDbUnitTest {
smsChannelService.deleteSmsChannel(id); smsChannelService.deleteSmsChannel(id);
// 校验数据不存在了 // 校验数据不存在了
assertNull(smsChannelMapper.selectById(id)); assertNull(smsChannelMapper.selectById(id));
// 断言 cache
assertNull(smsChannelService.getIdClientCache().getIfPresent(dbSmsChannel.getId()));
assertNull(smsChannelService.getCodeClientCache().getIfPresent(dbSmsChannel.getCode()));
} }
@Test @Test
@ -196,29 +187,23 @@ public class SmsChannelServiceTest extends BaseDbUnitTest {
// mock 数据 // mock 数据
SmsChannelDO channel = randomPojo(SmsChannelDO.class); SmsChannelDO channel = randomPojo(SmsChannelDO.class);
smsChannelMapper.insert(channel); smsChannelMapper.insert(channel);
// mock 参数 // 准备参数
Long id = channel.getId(); Long id = channel.getId();
// mock 方法 // mock 方法
SmsClient mockClient = mock(SmsClient.class); SmsClient mockClient = mock(SmsClient.class);
when(smsClientFactory.getSmsClient(eq(id))).thenReturn(mockClient); SmsChannelProperties properties = BeanUtils.toBean(channel, SmsChannelProperties.class);
when(smsClientFactory.createOrUpdateSmsClient(eq(properties))).thenReturn(mockClient);
// 调用 // 调用
SmsClient client = smsChannelService.getSmsClient(id); SmsClient client = smsChannelService.getSmsClient(id);
// 断言 // 断言
assertSame(client, mockClient); assertSame(client, mockClient);
verify(smsClientFactory).createOrUpdateSmsClient(argThat(arg -> {
SmsChannelProperties properties = BeanUtils.toBean(channel, SmsChannelProperties.class);
return properties.equals(arg);
}));
} }
@Test @Test
public void testGetSmsClient_code() { public void testGetSmsClient_code() {
// mock 数据 // 准备参数
SmsChannelDO channel = randomPojo(SmsChannelDO.class); String code = randomString();
smsChannelMapper.insert(channel);
// mock 参数
String code = channel.getCode();
// mock 方法 // mock 方法
SmsClient mockClient = mock(SmsClient.class); SmsClient mockClient = mock(SmsClient.class);
when(smsClientFactory.getSmsClient(eq(code))).thenReturn(mockClient); when(smsClientFactory.getSmsClient(eq(code))).thenReturn(mockClient);
@ -227,10 +212,6 @@ public class SmsChannelServiceTest extends BaseDbUnitTest {
SmsClient client = smsChannelService.getSmsClient(code); SmsClient client = smsChannelService.getSmsClient(code);
// 断言 // 断言
assertSame(client, mockClient); assertSame(client, mockClient);
verify(smsClientFactory).createOrUpdateSmsClient(argThat(arg -> {
SmsChannelProperties properties = BeanUtils.toBean(channel, SmsChannelProperties.class);
return properties.equals(arg);
}));
} }
} }

View File

@ -33,11 +33,11 @@
</dependency> </dependency>
<!-- 会员中心。默认注释,保证编译速度 --> <!-- 会员中心。默认注释,保证编译速度 -->
<!-- <dependency>--> <dependency>
<!-- <groupId>cn.iocoder.boot</groupId>--> <groupId>cn.iocoder.boot</groupId>
<!-- <artifactId>yudao-module-member-biz</artifactId>--> <artifactId>yudao-module-member-biz</artifactId>
<!-- <version>${revision}</version>--> <version>${revision}</version>
<!-- </dependency>--> </dependency>
<!-- 数据报表。默认注释,保证编译速度 --> <!-- 数据报表。默认注释,保证编译速度 -->
<!-- <dependency>--> <!-- <dependency>-->
@ -52,11 +52,11 @@
<!-- <version>${revision}</version>--> <!-- <version>${revision}</version>-->
<!-- </dependency>--> <!-- </dependency>-->
<!-- 支付服务。默认注释,保证编译速度 --> <!-- 支付服务。默认注释,保证编译速度 -->
<!-- <dependency>--> <dependency>
<!-- <groupId>cn.iocoder.boot</groupId>--> <groupId>cn.iocoder.boot</groupId>
<!-- <artifactId>yudao-module-pay-biz</artifactId>--> <artifactId>yudao-module-pay-biz</artifactId>
<!-- <version>${revision}</version>--> <version>${revision}</version>
<!-- </dependency>--> </dependency>
<!-- 微信公众号模块。默认注释,保证编译速度 --> <!-- 微信公众号模块。默认注释,保证编译速度 -->
<!-- <dependency>--> <!-- <dependency>-->
@ -66,26 +66,26 @@
<!-- </dependency>--> <!-- </dependency>-->
<!-- 商城相关模块。默认注释,保证编译速度--> <!-- 商城相关模块。默认注释,保证编译速度-->
<!-- <dependency>--> <dependency>
<!-- <groupId>cn.iocoder.boot</groupId>--> <groupId>cn.iocoder.boot</groupId>
<!-- <artifactId>yudao-module-promotion-biz</artifactId>--> <artifactId>yudao-module-promotion-biz</artifactId>
<!-- <version>${revision}</version>--> <version>${revision}</version>
<!-- </dependency>--> </dependency>
<!-- <dependency>--> <dependency>
<!-- <groupId>cn.iocoder.boot</groupId>--> <groupId>cn.iocoder.boot</groupId>
<!-- <artifactId>yudao-module-product-biz</artifactId>--> <artifactId>yudao-module-product-biz</artifactId>
<!-- <version>${revision}</version>--> <version>${revision}</version>
<!-- </dependency>--> </dependency>
<!-- <dependency>--> <dependency>
<!-- <groupId>cn.iocoder.boot</groupId>--> <groupId>cn.iocoder.boot</groupId>
<!-- <artifactId>yudao-module-trade-biz</artifactId>--> <artifactId>yudao-module-trade-biz</artifactId>
<!-- <version>${revision}</version>--> <version>${revision}</version>
<!-- </dependency>--> </dependency>
<!-- <dependency>--> <dependency>
<!-- <groupId>cn.iocoder.boot</groupId>--> <groupId>cn.iocoder.boot</groupId>
<!-- <artifactId>yudao-module-statistics-biz</artifactId>--> <artifactId>yudao-module-statistics-biz</artifactId>
<!-- <version>${revision}</version>--> <version>${revision}</version>
<!-- </dependency>--> </dependency>
<!-- CRM 相关模块。默认注释,保证编译速度 --> <!-- CRM 相关模块。默认注释,保证编译速度 -->
<!-- <dependency>--> <!-- <dependency>-->

View File

@ -40,12 +40,12 @@ spring:
primary: master primary: master
datasource: datasource:
master: master:
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 连接的示例 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 连接的示例
username: root username: root
password: 123456 password: 123456
slave: # 模拟从库,可根据自己需要修改 # 模拟从库,可根据自己需要修改 slave: # 模拟从库,可根据自己需要修改 # 模拟从库,可根据自己需要修改
lazy: true # 开启懒加载,保证启动速度 lazy: true # 开启懒加载,保证启动速度
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 连接的示例 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 连接的示例
username: root username: root
password: 123456 password: 123456

View File

@ -45,8 +45,8 @@ spring:
primary: master primary: master
datasource: datasource:
master: master:
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 连接的示例 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 连接的示例
# 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 连接的示例 # 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 连接的示例
# url: jdbc:postgresql://127.0.0.1:5432/ruoyi-vue-pro # PostgreSQL 连接的示例 # url: jdbc:postgresql://127.0.0.1:5432/ruoyi-vue-pro # PostgreSQL 连接的示例
# url: jdbc:oracle:thin:@127.0.0.1:1521:xe # Oracle 连接的示例 # url: jdbc:oracle:thin:@127.0.0.1:1521:xe # Oracle 连接的示例
# url: jdbc:sqlserver://127.0.0.1:1433;DatabaseName=ruoyi-vue-pro;SelectMethod=cursor;encrypt=false;rewriteBatchedStatements=true;useUnicode=true;characterEncoding=utf-8 # SQLServer 连接的示例 # 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:
# password: Yudao@2024 # OpenGauss 连接的示例 # password: Yudao@2024 # OpenGauss 连接的示例
slave: # 模拟从库,可根据自己需要修改 slave: # 模拟从库,可根据自己需要修改
lazy: true # 开启懒加载,保证启动速度 lazy: true # 开启懒加载,保证启动速度
url: jdbc:mysql://127.0.0.1:3306/ruoyi-vue-pro?useSSL=false&serverTimezone=Asia/Shanghai&allowPublicKeyRetrieval=true&nullCatalogMeansCurrent=true url: jdbc:mysql://127.0.0.1:3306/ruoyi-vue-pro?useSSL=false&serverTimezone=Asia/Shanghai&allowPublicKeyRetrieval=true&rewriteBatchedStatements=true&nullCatalogMeansCurrent=true
username: root username: root
password: 123456 password: 123456

View File

@ -301,6 +301,7 @@ yudao:
- tmp_report_data_1 - tmp_report_data_1
- tmp_report_data_income - tmp_report_data_income
ignore-caches: ignore-caches:
- user_role_ids
- permission_menu_ids - permission_menu_ids
- oauth_client - oauth_client
- notify_template - notify_template